diff --git a/examples/Operator/Controller/V1TestEntityController.cs b/examples/Operator/Controller/V1TestEntityController.cs index 65152824..7b43e2f7 100644 --- a/examples/Operator/Controller/V1TestEntityController.cs +++ b/examples/Operator/Controller/V1TestEntityController.cs @@ -19,13 +19,13 @@ public V1TestEntityController(ILogger logger) public Task ReconcileAsync(V1TestEntity entity) { - _logger.LogInformation("Reconciling entity {EntityName}.", entity.Metadata.Name); + _logger.LogInformation("Reconciling entity {Entity}.", entity); return Task.CompletedTask; } public Task DeletedAsync(V1TestEntity entity) { - _logger.LogInformation("Deleting entity {EntityName}.", entity.Metadata.Name); + _logger.LogInformation("Deleting entity {Entity}.", entity); return Task.CompletedTask; } } diff --git a/examples/Operator/Entities/V1TestEntity.cs b/examples/Operator/Entities/V1TestEntity.cs index c6eb1240..9af15b23 100644 --- a/examples/Operator/Entities/V1TestEntity.cs +++ b/examples/Operator/Entities/V1TestEntity.cs @@ -15,4 +15,6 @@ public class V1TestEntity : IKubernetesObject, ISpec $"Test Entity ({Metadata.Name}): {Spec.Username} ({Spec.Email})"; } diff --git a/examples/Operator/Entities/V1TestEntitySpec.cs b/examples/Operator/Entities/V1TestEntitySpec.cs index 3c1c0f98..4cb2b937 100644 --- a/examples/Operator/Entities/V1TestEntitySpec.cs +++ b/examples/Operator/Entities/V1TestEntitySpec.cs @@ -1,12 +1,8 @@ -using k8s.Models; - namespace Operator.Entities; public class V1TestEntitySpec { - public string Spec { get; set; } = string.Empty; - public string Username { get; set; } = string.Empty; - public IntstrIntOrString StringOrInteger { get; set; } = 42; + public string Email { get; set; } = string.Empty; } diff --git a/examples/Operator/Program.cs b/examples/Operator/Program.cs index c896f175..92d03697 100644 --- a/examples/Operator/Program.cs +++ b/examples/Operator/Program.cs @@ -3,17 +3,13 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Operator.Controller; -using Operator.Entities; - var builder = Host.CreateApplicationBuilder(args); builder.Logging.SetMinimumLevel(LogLevel.Trace); builder.Services .AddKubernetesOperator() - .RegisterEntitiyMetadata() - .AddController(); + .RegisterResources(); using var host = builder.Build(); await host.RunAsync(); diff --git a/examples/Operator/test_entity.yaml b/examples/Operator/test_entity.yaml index d602ec3d..580f52c0 100644 --- a/examples/Operator/test_entity.yaml +++ b/examples/Operator/test_entity.yaml @@ -2,3 +2,6 @@ apiVersion: testing.dev/v1 kind: TestEntity metadata: name: my-test-entity +spec: + username: my-username + email: foobar@test.ch diff --git a/examples/Operator/todos.txt b/examples/Operator/todos.txt new file mode 100644 index 00000000..a64eea16 --- /dev/null +++ b/examples/Operator/todos.txt @@ -0,0 +1,10 @@ +todo: +- finalizer +- events +- leadership election +- requeue +- build targets +- other CLI commands +- web: webhooks +- cache? +- try .net 8 AOT? diff --git a/src/KubeOps.Abstractions/Entities/CustomKubernetesEntity.cs b/src/KubeOps.Abstractions/Entities/CustomKubernetesEntity.cs index 90feafef..7c453c75 100644 --- a/src/KubeOps.Abstractions/Entities/CustomKubernetesEntity.cs +++ b/src/KubeOps.Abstractions/Entities/CustomKubernetesEntity.cs @@ -1,16 +1,16 @@ -using k8s; -using k8s.Models; - -namespace KubeOps.Abstractions.Entities; - -/// -/// Base class for custom Kubernetes entities. The interface -/// can be used on its own, but this class provides convenience initializers. -/// -public abstract class CustomKubernetesEntity : KubernetesObject, IKubernetesObject -{ - /// - /// The metadata of the kubernetes object. - /// - public V1ObjectMeta Metadata { get; set; } = new(); -} +using k8s; +using k8s.Models; + +namespace KubeOps.Abstractions.Entities; + +/// +/// Base class for custom Kubernetes entities. The interface +/// can be used on its own, but this class provides convenience initializers. +/// +public abstract class CustomKubernetesEntity : KubernetesObject, IKubernetesObject +{ + /// + /// The metadata of the kubernetes object. + /// + public V1ObjectMeta Metadata { get; set; } = new(); +} diff --git a/src/KubeOps.Abstractions/Entities/CustomKubernetesEntity{TSpec,TStatus}.cs b/src/KubeOps.Abstractions/Entities/CustomKubernetesEntity{TSpec,TStatus}.cs index 28d8ce98..295824c0 100644 --- a/src/KubeOps.Abstractions/Entities/CustomKubernetesEntity{TSpec,TStatus}.cs +++ b/src/KubeOps.Abstractions/Entities/CustomKubernetesEntity{TSpec,TStatus}.cs @@ -1,21 +1,21 @@ -using k8s; - -namespace KubeOps.Abstractions.Entities; - -/// -/// Defines a custom Kubernetes entity. -/// This entity contains a spec (like ) -/// and a status () which can be updated to reflect the state -/// of the entity. -/// -/// The type of the specified data. -/// The type of the status data. -public abstract class CustomKubernetesEntity : CustomKubernetesEntity, IStatus - where TSpec : new() - where TStatus : new() -{ - /// - /// Status object for the entity. - /// - public TStatus Status { get; set; } = new(); -} +using k8s; + +namespace KubeOps.Abstractions.Entities; + +/// +/// Defines a custom Kubernetes entity. +/// This entity contains a spec (like ) +/// and a status () which can be updated to reflect the state +/// of the entity. +/// +/// The type of the specified data. +/// The type of the status data. +public abstract class CustomKubernetesEntity : CustomKubernetesEntity, IStatus + where TSpec : new() + where TStatus : new() +{ + /// + /// Status object for the entity. + /// + public TStatus Status { get; set; } = new(); +} diff --git a/src/KubeOps.Abstractions/Entities/CustomKubernetesEntity{TSpec}.cs b/src/KubeOps.Abstractions/Entities/CustomKubernetesEntity{TSpec}.cs index d9d13e83..5a0353d4 100644 --- a/src/KubeOps.Abstractions/Entities/CustomKubernetesEntity{TSpec}.cs +++ b/src/KubeOps.Abstractions/Entities/CustomKubernetesEntity{TSpec}.cs @@ -1,17 +1,17 @@ -using k8s; - -namespace KubeOps.Abstractions.Entities; - -/// -/// Defines a custom kubernetes entity which can be used in finalizers and controllers. -/// This entity contains a , which means in contains specified data. -/// -/// The type of the specified data. -public abstract class CustomKubernetesEntity : CustomKubernetesEntity, ISpec - where TSpec : new() -{ - /// - /// Specification of the kubernetes object. - /// - public TSpec Spec { get; set; } = new(); -} +using k8s; + +namespace KubeOps.Abstractions.Entities; + +/// +/// Defines a custom kubernetes entity which can be used in finalizers and controllers. +/// This entity contains a , which means in contains specified data. +/// +/// The type of the specified data. +public abstract class CustomKubernetesEntity : CustomKubernetesEntity, ISpec + where TSpec : new() +{ + /// + /// Specification of the kubernetes object. + /// + public TSpec Spec { get; set; } = new(); +} diff --git a/src/KubeOps.Abstractions/Entities/EntityList.cs b/src/KubeOps.Abstractions/Entities/EntityList.cs index 5071e7eb..769f917e 100644 --- a/src/KubeOps.Abstractions/Entities/EntityList.cs +++ b/src/KubeOps.Abstractions/Entities/EntityList.cs @@ -1,22 +1,22 @@ -using k8s; -using k8s.Models; - -namespace KubeOps.Abstractions.Entities; - -/// -/// Type for a list of entities. -/// -/// Type for the list entries. -public class EntityList : KubernetesObject - where T : IKubernetesObject -{ - /// - /// Official list metadata object of kubernetes. - /// - public V1ListMeta Metadata { get; set; } = new(); - - /// - /// The list of items. - /// - public IList Items { get; set; } = new List(); -} +using k8s; +using k8s.Models; + +namespace KubeOps.Abstractions.Entities; + +/// +/// Type for a list of entities. +/// +/// Type for the list entries. +public class EntityList : KubernetesObject + where T : IKubernetesObject +{ + /// + /// Official list metadata object of kubernetes. + /// + public V1ListMeta Metadata { get; set; } = new(); + + /// + /// The list of items. + /// + public IList Items { get; set; } = new List(); +} diff --git a/src/KubeOps.Abstractions/Entities/Extensions.cs b/src/KubeOps.Abstractions/Entities/Extensions.cs index 06045853..1a5a9b36 100644 --- a/src/KubeOps.Abstractions/Entities/Extensions.cs +++ b/src/KubeOps.Abstractions/Entities/Extensions.cs @@ -1,42 +1,42 @@ -using k8s; -using k8s.Models; - -namespace KubeOps.Abstractions.Entities; - -/// -/// Method extensions for . -/// -public static class Extensions -{ - /// - /// Sets the resource version of the specified Kubernetes object to the specified value. - /// - /// The type of the Kubernetes object. - /// The Kubernetes object. - /// The resource version to set. - /// The Kubernetes object with the updated resource version. - public static TEntity WithResourceVersion( - this TEntity entity, - string resourceVersion) - where TEntity : IKubernetesObject - { - entity.EnsureMetadata().ResourceVersion = resourceVersion; - return entity; - } - - /// - /// Sets the resource version of the specified Kubernetes object to the resource version of another object. - /// - /// The type of the Kubernetes object. - /// The Kubernetes object. - /// The other Kubernetes object. - /// The Kubernetes object with the updated resource version. - public static TEntity WithResourceVersion( - this TEntity entity, - TEntity other) - where TEntity : IKubernetesObject - { - entity.EnsureMetadata().ResourceVersion = other.ResourceVersion(); - return entity; - } -} +using k8s; +using k8s.Models; + +namespace KubeOps.Abstractions.Entities; + +/// +/// Method extensions for . +/// +public static class Extensions +{ + /// + /// Sets the resource version of the specified Kubernetes object to the specified value. + /// + /// The type of the Kubernetes object. + /// The Kubernetes object. + /// The resource version to set. + /// The Kubernetes object with the updated resource version. + public static TEntity WithResourceVersion( + this TEntity entity, + string resourceVersion) + where TEntity : IKubernetesObject + { + entity.EnsureMetadata().ResourceVersion = resourceVersion; + return entity; + } + + /// + /// Sets the resource version of the specified Kubernetes object to the resource version of another object. + /// + /// The type of the Kubernetes object. + /// The Kubernetes object. + /// The other Kubernetes object. + /// The Kubernetes object with the updated resource version. + public static TEntity WithResourceVersion( + this TEntity entity, + TEntity other) + where TEntity : IKubernetesObject + { + entity.EnsureMetadata().ResourceVersion = other.ResourceVersion(); + return entity; + } +} diff --git a/src/KubeOps.Abstractions/Kustomize/KustomizationConfig.cs b/src/KubeOps.Abstractions/Kustomize/KustomizationConfig.cs index 8e0d563e..618ab137 100644 --- a/src/KubeOps.Abstractions/Kustomize/KustomizationConfig.cs +++ b/src/KubeOps.Abstractions/Kustomize/KustomizationConfig.cs @@ -1,50 +1,50 @@ -using k8s; - -namespace KubeOps.Abstractions.Kustomize; - -/// -/// (Partial) definition for a kustomization yaml. -/// -public class KustomizationConfig : KubernetesObject -{ - public KustomizationConfig() - { - ApiVersion = "kustomize.config.k8s.io/v1beta1"; - Kind = "Kustomization"; - } - - /// - /// Namespace that should be set. - /// - public string? Namespace { get; set; } - - /// - /// Name prefix that should be set. - /// - public string? NamePrefix { get; set; } - - /// - /// Common labels for the resources. - /// - public IDictionary? CommonLabels { get; set; } - - /// - /// Resource list. - /// - public IList? Resources { get; set; } - - /// - /// List of merge patches. - /// - public IList? PatchesStrategicMerge { get; set; } - - /// - /// List of . - /// - public IList? Images { get; set; } - - /// - /// List of . - /// - public IList? ConfigMapGenerator { get; set; } -} +using k8s; + +namespace KubeOps.Abstractions.Kustomize; + +/// +/// (Partial) definition for a kustomization yaml. +/// +public class KustomizationConfig : KubernetesObject +{ + public KustomizationConfig() + { + ApiVersion = "kustomize.config.k8s.io/v1beta1"; + Kind = "Kustomization"; + } + + /// + /// Namespace that should be set. + /// + public string? Namespace { get; set; } + + /// + /// Name prefix that should be set. + /// + public string? NamePrefix { get; set; } + + /// + /// Common labels for the resources. + /// + public IDictionary? CommonLabels { get; set; } + + /// + /// Resource list. + /// + public IList? Resources { get; set; } + + /// + /// List of merge patches. + /// + public IList? PatchesStrategicMerge { get; set; } + + /// + /// List of . + /// + public IList? Images { get; set; } + + /// + /// List of . + /// + public IList? ConfigMapGenerator { get; set; } +} diff --git a/src/KubeOps.Abstractions/Kustomize/KustomizationConfigMapGenerator.cs b/src/KubeOps.Abstractions/Kustomize/KustomizationConfigMapGenerator.cs index 75c39159..3207e18b 100644 --- a/src/KubeOps.Abstractions/Kustomize/KustomizationConfigMapGenerator.cs +++ b/src/KubeOps.Abstractions/Kustomize/KustomizationConfigMapGenerator.cs @@ -1,23 +1,23 @@ -namespace KubeOps.Abstractions.Kustomize; - -/// -/// Entitiy for config map generators in a kustomization.yaml file. -/// -public class KustomizationConfigMapGenerator -{ - /// - /// 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 KustomizationConfigMapGenerator +{ + /// + /// 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.Abstractions/Kustomize/KustomizationImage.cs b/src/KubeOps.Abstractions/Kustomize/KustomizationImage.cs index dc0e1009..ecef2b61 100644 --- a/src/KubeOps.Abstractions/Kustomize/KustomizationImage.cs +++ b/src/KubeOps.Abstractions/Kustomize/KustomizationImage.cs @@ -1,22 +1,22 @@ -namespace KubeOps.Abstractions.Kustomize; - -/// -/// Definition for an "image" in a kustomization yaml. -/// -public class KustomizationImage -{ - /// - /// Name of the image. - /// - public string Name { get; set; } = string.Empty; - - /// - /// New name of the image. - /// - public string NewName { get; set; } = string.Empty; - - /// - /// New tag of the image. - /// - public string NewTag { get; set; } = string.Empty; -} +namespace KubeOps.Abstractions.Kustomize; + +/// +/// Definition for an "image" in a kustomization yaml. +/// +public class KustomizationImage +{ + /// + /// Name of the image. + /// + public string Name { get; set; } = string.Empty; + + /// + /// New name of the image. + /// + public string NewName { get; set; } = string.Empty; + + /// + /// New tag of the image. + /// + public string NewTag { get; set; } = string.Empty; +} diff --git a/src/KubeOps.Cli/Arguments.cs b/src/KubeOps.Cli/Arguments.cs index b7baf3ce..12d768bc 100644 --- a/src/KubeOps.Cli/Arguments.cs +++ b/src/KubeOps.Cli/Arguments.cs @@ -1,44 +1,44 @@ -using System.CommandLine; - -namespace KubeOps.Cli; - -internal static class Arguments -{ - public static readonly Argument SolutionOrProjectFile = new( - "sln/csproj file", - () => - { - var projectFile - = Directory.EnumerateFiles( - Directory.GetCurrentDirectory(), - "*.csproj") - .Select(f => new FileInfo(f)) - .FirstOrDefault(); - var slnFile - = Directory.EnumerateFiles( - Directory.GetCurrentDirectory(), - "*.sln") - .Select(f => new FileInfo(f)) - .FirstOrDefault(); - - return (projectFile, slnFile) switch - { - ({ } prj, _) => prj, - (_, { } sln) => sln, - _ => null, - }; - }, - "A solution or project file where entities are located. " + - "If omitted, the current directory is searched for a *.csproj or *.sln file. " + - "If an *.sln file is used, all projects in the solution (with the newest framework) will be searched for entities. " + - "This behaviour can be filtered by using the --project and --target-framework option."); - - public static readonly Argument CertificateServerName = new( - "name", - "The server name for the certificate (name of the service/deployment)."); - - public static readonly Argument CertificateServerNamespace = new( - "namespace", - () => "default", - "The Kubernetes namespace that the operator will be run."); -} +using System.CommandLine; + +namespace KubeOps.Cli; + +internal static class Arguments +{ + public static readonly Argument SolutionOrProjectFile = new( + "sln/csproj file", + () => + { + var projectFile + = Directory.EnumerateFiles( + Directory.GetCurrentDirectory(), + "*.csproj") + .Select(f => new FileInfo(f)) + .FirstOrDefault(); + var slnFile + = Directory.EnumerateFiles( + Directory.GetCurrentDirectory(), + "*.sln") + .Select(f => new FileInfo(f)) + .FirstOrDefault(); + + return (projectFile, slnFile) switch + { + ({ } prj, _) => prj, + (_, { } sln) => sln, + _ => null, + }; + }, + "A solution or project file where entities are located. " + + "If omitted, the current directory is searched for a *.csproj or *.sln file. " + + "If an *.sln file is used, all projects in the solution (with the newest framework) will be searched for entities. " + + "This behaviour can be filtered by using the --project and --target-framework option."); + + public static readonly Argument CertificateServerName = new( + "name", + "The server name for the certificate (name of the service/deployment)."); + + public static readonly Argument CertificateServerNamespace = new( + "namespace", + () => "default", + "The Kubernetes namespace that the operator will be run."); +} diff --git a/src/KubeOps.Cli/Commands/Generator/CertificateGenerator.cs b/src/KubeOps.Cli/Commands/Generator/CertificateGenerator.cs index 82dd5214..2e2942c5 100644 --- a/src/KubeOps.Cli/Commands/Generator/CertificateGenerator.cs +++ b/src/KubeOps.Cli/Commands/Generator/CertificateGenerator.cs @@ -1,192 +1,192 @@ -using System.CommandLine; -using System.CommandLine.Invocation; -using System.Text; - -using KubeOps.Cli.Output; - -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.OpenSsl; -using Org.BouncyCastle.Security; -using Org.BouncyCastle.Utilities; -using Org.BouncyCastle.X509; -using Org.BouncyCastle.X509.Extension; - -using Spectre.Console; - -namespace KubeOps.Cli.Commands.Generator; - -internal static class CertificateGenerator -{ - public static Command Command - { - get - { - var cmd = new Command("certificates", "Generates a CA and a server certificate.") - { - Options.OutputPath, Arguments.CertificateServerName, Arguments.CertificateServerNamespace, - }; - cmd.AddAlias("cert"); - cmd.SetHandler(ctx => Handler(AnsiConsole.Console, ctx)); - - return cmd; - } - } - - internal static async Task Handler(IAnsiConsole console, InvocationContext ctx) - { - var outPath = ctx.ParseResult.GetValueForOption(Options.OutputPath); - var result = new ResultOutput(console, OutputFormat.Plain); - - console.MarkupLine("Generate [cyan]CA[/] certificate and private key."); - var (caCert, caKey) = CreateCaCertificate(); - - result.Add("ca.pem", ToPem(caCert)); - result.Add("ca-key.pem", ToPem(caKey)); - - console.MarkupLine("Generate [cyan]server[/] certificate and private key."); - var (srvCert, srvKey) = CreateServerCertificate( - (caCert, caKey), - ctx.ParseResult.GetValueForArgument(Arguments.CertificateServerName), - ctx.ParseResult.GetValueForArgument(Arguments.CertificateServerNamespace)); - - result.Add("svc.pem", ToPem(srvCert)); - result.Add("svc-key.pem", ToPem(srvKey)); - - if (outPath is not null) - { - await result.Write(outPath); - } - else - { - result.Write(); - } - } - - private static string ToPem(object obj) - { - var sb = new StringBuilder(); - using var writer = new PemWriter(new StringWriter(sb)); - writer.WriteObject(obj); - return sb.ToString(); - } - - private 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); - } - - private 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 System.CommandLine; +using System.CommandLine.Invocation; +using System.Text; + +using KubeOps.Cli.Output; + +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.OpenSsl; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.X509.Extension; + +using Spectre.Console; + +namespace KubeOps.Cli.Commands.Generator; + +internal static class CertificateGenerator +{ + public static Command Command + { + get + { + var cmd = new Command("certificates", "Generates a CA and a server certificate.") + { + Options.OutputPath, Arguments.CertificateServerName, Arguments.CertificateServerNamespace, + }; + cmd.AddAlias("cert"); + cmd.SetHandler(ctx => Handler(AnsiConsole.Console, ctx)); + + return cmd; + } + } + + internal static async Task Handler(IAnsiConsole console, InvocationContext ctx) + { + var outPath = ctx.ParseResult.GetValueForOption(Options.OutputPath); + var result = new ResultOutput(console, OutputFormat.Plain); + + console.MarkupLine("Generate [cyan]CA[/] certificate and private key."); + var (caCert, caKey) = CreateCaCertificate(); + + result.Add("ca.pem", ToPem(caCert)); + result.Add("ca-key.pem", ToPem(caKey)); + + console.MarkupLine("Generate [cyan]server[/] certificate and private key."); + var (srvCert, srvKey) = CreateServerCertificate( + (caCert, caKey), + ctx.ParseResult.GetValueForArgument(Arguments.CertificateServerName), + ctx.ParseResult.GetValueForArgument(Arguments.CertificateServerNamespace)); + + result.Add("svc.pem", ToPem(srvCert)); + result.Add("svc-key.pem", ToPem(srvKey)); + + if (outPath is not null) + { + await result.Write(outPath); + } + else + { + result.Write(); + } + } + + private static string ToPem(object obj) + { + var sb = new StringBuilder(); + using var writer = new PemWriter(new StringWriter(sb)); + writer.WriteObject(obj); + return sb.ToString(); + } + + private 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); + } + + private 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/Commands/Management/Install.cs b/src/KubeOps.Cli/Commands/Management/Install.cs index 4534abaa..2c1cead7 100644 --- a/src/KubeOps.Cli/Commands/Management/Install.cs +++ b/src/KubeOps.Cli/Commands/Management/Install.cs @@ -1,112 +1,112 @@ -using System.CommandLine; -using System.CommandLine.Invocation; - -using k8s; -using k8s.Autorest; -using k8s.Models; - -using KubeOps.Cli.Roslyn; -using KubeOps.Transpiler; - -using Spectre.Console; - -namespace KubeOps.Cli.Commands.Management; - -internal static class Install -{ - public static Command Command - { - get - { - var cmd = - new Command("install", "Install CRDs into the cluster of the actually selected context.") - { - Options.Force, - Options.SolutionProjectRegex, - Options.TargetFramework, - Arguments.SolutionOrProjectFile, - }; - cmd.AddAlias("i"); - cmd.SetHandler(ctx => Handler( - AnsiConsole.Console, - new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()), - ctx)); - - return cmd; - } - } - - internal static async Task Handler(IAnsiConsole console, IKubernetes client, InvocationContext ctx) - { - var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile); - var force = ctx.ParseResult.GetValueForOption(Options.Force); - - var parser = file switch - { - { Extension: ".csproj", Exists: true } => await AssemblyParser.ForProject(console, file), - { Extension: ".sln", Exists: true } => await AssemblyParser.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."), - }; - - console.WriteLine($"Install CRDs from {file.Name}."); - var crds = Crds.Transpile(parser.Entities()).ToList(); - if (crds.Count == 0) - { - console.WriteLine("No CRDs found. Exiting."); - ctx.ExitCode = ExitCodes.Success; - return; - } - - console.WriteLine($"Found {crds.Count} CRDs."); - console.WriteLine($"""Starting install into cluster with url "{client.BaseUri}"."""); - - foreach (var crd in crds) - { - console.MarkupLineInterpolated( - $"""Install [cyan]"{crd.Spec.Group}/{crd.Spec.Names.Kind}"[/] into the cluster."""); - - try - { - switch (await client.ApiextensionsV1.ListCustomResourceDefinitionAsync( - fieldSelector: $"metadata.name={crd.Name()}")) - { - 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?[/]")) - { - ctx.ExitCode = ExitCodes.Aborted; - return; - } - - crd.Metadata.ResourceVersion = existing.ResourceVersion(); - await client.ApiextensionsV1.ReplaceCustomResourceDefinitionAsync(crd, crd.Name()); - break; - default: - await client.ApiextensionsV1.CreateCustomResourceDefinitionAsync(crd); - break; - } - - console.MarkupLineInterpolated( - $"""[green]Installed / Updated CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); - } - catch (HttpOperationException) - { - console.WriteLine( - $"""[red]There was a http (api) error while installing "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); - throw; - } - catch (Exception) - { - console.WriteLine( - $"""[red]There was an error while installing "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); - throw; - } - } - } -} +using System.CommandLine; +using System.CommandLine.Invocation; + +using k8s; +using k8s.Autorest; +using k8s.Models; + +using KubeOps.Cli.Roslyn; +using KubeOps.Transpiler; + +using Spectre.Console; + +namespace KubeOps.Cli.Commands.Management; + +internal static class Install +{ + public static Command Command + { + get + { + var cmd = + new Command("install", "Install CRDs into the cluster of the actually selected context.") + { + Options.Force, + Options.SolutionProjectRegex, + Options.TargetFramework, + Arguments.SolutionOrProjectFile, + }; + cmd.AddAlias("i"); + cmd.SetHandler(ctx => Handler( + AnsiConsole.Console, + new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()), + ctx)); + + return cmd; + } + } + + internal static async Task Handler(IAnsiConsole console, IKubernetes client, InvocationContext ctx) + { + var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile); + var force = ctx.ParseResult.GetValueForOption(Options.Force); + + var parser = file switch + { + { Extension: ".csproj", Exists: true } => await AssemblyParser.ForProject(console, file), + { Extension: ".sln", Exists: true } => await AssemblyParser.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."), + }; + + console.WriteLine($"Install CRDs from {file.Name}."); + var crds = Crds.Transpile(parser.Entities()).ToList(); + if (crds.Count == 0) + { + console.WriteLine("No CRDs found. Exiting."); + ctx.ExitCode = ExitCodes.Success; + return; + } + + console.WriteLine($"Found {crds.Count} CRDs."); + console.WriteLine($"""Starting install into cluster with url "{client.BaseUri}"."""); + + foreach (var crd in crds) + { + console.MarkupLineInterpolated( + $"""Install [cyan]"{crd.Spec.Group}/{crd.Spec.Names.Kind}"[/] into the cluster."""); + + try + { + switch (await client.ApiextensionsV1.ListCustomResourceDefinitionAsync( + fieldSelector: $"metadata.name={crd.Name()}")) + { + 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?[/]")) + { + ctx.ExitCode = ExitCodes.Aborted; + return; + } + + crd.Metadata.ResourceVersion = existing.ResourceVersion(); + await client.ApiextensionsV1.ReplaceCustomResourceDefinitionAsync(crd, crd.Name()); + break; + default: + await client.ApiextensionsV1.CreateCustomResourceDefinitionAsync(crd); + break; + } + + console.MarkupLineInterpolated( + $"""[green]Installed / Updated CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); + } + catch (HttpOperationException) + { + console.WriteLine( + $"""[red]There was a http (api) error while installing "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); + throw; + } + catch (Exception) + { + console.WriteLine( + $"""[red]There was an error while installing "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); + throw; + } + } + } +} diff --git a/src/KubeOps.Cli/Commands/Management/Uninstall.cs b/src/KubeOps.Cli/Commands/Management/Uninstall.cs index 1212f16c..b5be2739 100644 --- a/src/KubeOps.Cli/Commands/Management/Uninstall.cs +++ b/src/KubeOps.Cli/Commands/Management/Uninstall.cs @@ -1,109 +1,109 @@ -using System.CommandLine; -using System.CommandLine.Invocation; - -using k8s; -using k8s.Autorest; -using k8s.Models; - -using KubeOps.Cli.Roslyn; -using KubeOps.Transpiler; - -using Spectre.Console; - -namespace KubeOps.Cli.Commands.Management; - -internal static class Uninstall -{ - public static Command Command - { - get - { - var cmd = - new Command("uninstall", "Uninstall CRDs from the cluster of the actually selected context.") - { - Options.Force, - Options.SolutionProjectRegex, - Options.TargetFramework, - Arguments.SolutionOrProjectFile, - }; - cmd.AddAlias("u"); - cmd.SetHandler(ctx => Handler( - AnsiConsole.Console, - new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()), - ctx)); - - return cmd; - } - } - - internal static async Task Handler(IAnsiConsole console, IKubernetes client, InvocationContext ctx) - { - var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile); - var force = ctx.ParseResult.GetValueForOption(Options.Force); - - var parser = file switch - { - { Extension: ".csproj", Exists: true } => await AssemblyParser.ForProject(console, file), - { Extension: ".sln", Exists: true } => await AssemblyParser.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."), - }; - - console.WriteLine($"Uninstall CRDs from {file.Name}."); - var crds = Crds.Transpile(parser.Entities()).ToList(); - if (crds.Count == 0) - { - console.WriteLine("No CRDs found. Exiting."); - ctx.ExitCode = ExitCodes.Success; - return; - } - - console.WriteLine($"Found {crds.Count} CRDs."); - if (!force && !console.Confirm("[red]Should the CRDs be uninstalled?[/]", false)) - { - ctx.ExitCode = ExitCodes.Aborted; - return; - } - - console.WriteLine($"""Starting uninstall from cluster with url "{client.BaseUri}"."""); - - foreach (var crd in crds) - { - console.MarkupLineInterpolated( - $"""Uninstall [cyan]"{crd.Spec.Group}/{crd.Spec.Names.Kind}"[/] from the cluster."""); - - try - { - switch (await client.ApiextensionsV1.ListCustomResourceDefinitionAsync( - fieldSelector: $"metadata.name={crd.Name()}")) - { - case { Items: [var existing] }: - await client.ApiextensionsV1.DeleteCustomResourceDefinitionAsync(existing.Name()); - console.MarkupLineInterpolated( - $"""[green]CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}" deleted.[/]"""); - break; - default: - console.MarkupLineInterpolated( - $"""[green]CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}" did not exist.[/]"""); - break; - } - } - catch (HttpOperationException) - { - console.WriteLine( - $"""[red]There was a http (api) error while uninstalling "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); - throw; - } - catch (Exception) - { - console.WriteLine( - $"""[red]There was an error while uninstalling "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); - throw; - } - } - } -} +using System.CommandLine; +using System.CommandLine.Invocation; + +using k8s; +using k8s.Autorest; +using k8s.Models; + +using KubeOps.Cli.Roslyn; +using KubeOps.Transpiler; + +using Spectre.Console; + +namespace KubeOps.Cli.Commands.Management; + +internal static class Uninstall +{ + public static Command Command + { + get + { + var cmd = + new Command("uninstall", "Uninstall CRDs from the cluster of the actually selected context.") + { + Options.Force, + Options.SolutionProjectRegex, + Options.TargetFramework, + Arguments.SolutionOrProjectFile, + }; + cmd.AddAlias("u"); + cmd.SetHandler(ctx => Handler( + AnsiConsole.Console, + new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()), + ctx)); + + return cmd; + } + } + + internal static async Task Handler(IAnsiConsole console, IKubernetes client, InvocationContext ctx) + { + var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile); + var force = ctx.ParseResult.GetValueForOption(Options.Force); + + var parser = file switch + { + { Extension: ".csproj", Exists: true } => await AssemblyParser.ForProject(console, file), + { Extension: ".sln", Exists: true } => await AssemblyParser.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."), + }; + + console.WriteLine($"Uninstall CRDs from {file.Name}."); + var crds = Crds.Transpile(parser.Entities()).ToList(); + if (crds.Count == 0) + { + console.WriteLine("No CRDs found. Exiting."); + ctx.ExitCode = ExitCodes.Success; + return; + } + + console.WriteLine($"Found {crds.Count} CRDs."); + if (!force && !console.Confirm("[red]Should the CRDs be uninstalled?[/]", false)) + { + ctx.ExitCode = ExitCodes.Aborted; + return; + } + + console.WriteLine($"""Starting uninstall from cluster with url "{client.BaseUri}"."""); + + foreach (var crd in crds) + { + console.MarkupLineInterpolated( + $"""Uninstall [cyan]"{crd.Spec.Group}/{crd.Spec.Names.Kind}"[/] from the cluster."""); + + try + { + switch (await client.ApiextensionsV1.ListCustomResourceDefinitionAsync( + fieldSelector: $"metadata.name={crd.Name()}")) + { + case { Items: [var existing] }: + await client.ApiextensionsV1.DeleteCustomResourceDefinitionAsync(existing.Name()); + console.MarkupLineInterpolated( + $"""[green]CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}" deleted.[/]"""); + break; + default: + console.MarkupLineInterpolated( + $"""[green]CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}" did not exist.[/]"""); + break; + } + } + catch (HttpOperationException) + { + console.WriteLine( + $"""[red]There was a http (api) error while uninstalling "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); + throw; + } + catch (Exception) + { + console.WriteLine( + $"""[red]There was an error while uninstalling "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); + throw; + } + } + } +} diff --git a/src/KubeOps.Cli/Options.cs b/src/KubeOps.Cli/Options.cs index 727c0aa7..25138cfc 100644 --- a/src/KubeOps.Cli/Options.cs +++ b/src/KubeOps.Cli/Options.cs @@ -1,38 +1,38 @@ -using System.CommandLine; -using System.Text.RegularExpressions; - -using KubeOps.Cli.Output; - -namespace KubeOps.Cli; - -internal static class Options -{ - public static readonly Option OutputFormat = new( - "--format", - () => Output.OutputFormat.Yaml, - "The format of the generated output."); - - public static readonly Option OutputPath = new( - "--out", - "The path the command will write the files to. If omitted, prints output to console."); - - public static readonly Option TargetFramework = new( - new[] { "--target-framework", "--tfm" }, - description: "Target framework of projects in the solution to search for entities. " + - "If omitted, the newest framework is used."); - - public static readonly Option SolutionProjectRegex = new( - "--project", - parseArgument: result => - { - var value = result.Tokens.Single().Value; - return new Regex(value); - }, - description: "Regex pattern to filter projects in the solution to search for entities. " + - "If omitted, all projects are searched."); - - public static readonly Option Force = new( - new[] { "--force", "-f" }, - () => false, - description: "Do not bother the user with questions and just do it."); -} +using System.CommandLine; +using System.Text.RegularExpressions; + +using KubeOps.Cli.Output; + +namespace KubeOps.Cli; + +internal static class Options +{ + public static readonly Option OutputFormat = new( + "--format", + () => Output.OutputFormat.Yaml, + "The format of the generated output."); + + public static readonly Option OutputPath = new( + "--out", + "The path the command will write the files to. If omitted, prints output to console."); + + public static readonly Option TargetFramework = new( + new[] { "--target-framework", "--tfm" }, + description: "Target framework of projects in the solution to search for entities. " + + "If omitted, the newest framework is used."); + + public static readonly Option SolutionProjectRegex = new( + "--project", + parseArgument: result => + { + var value = result.Tokens.Single().Value; + return new Regex(value); + }, + description: "Regex pattern to filter projects in the solution to search for entities. " + + "If omitted, all projects are searched."); + + public static readonly Option Force = new( + new[] { "--force", "-f" }, + () => false, + description: "Do not bother the user with questions and just do it."); +} diff --git a/src/KubeOps.Cli/Roslyn/AssemblyParser.cs b/src/KubeOps.Cli/Roslyn/AssemblyParser.cs index 1cacdf26..672f381d 100644 --- a/src/KubeOps.Cli/Roslyn/AssemblyParser.cs +++ b/src/KubeOps.Cli/Roslyn/AssemblyParser.cs @@ -1,157 +1,157 @@ -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; - -using k8s.Models; - -using KubeOps.Abstractions.Entities.Attributes; -using KubeOps.Abstractions.Rbac; - -using Microsoft.Build.Locator; -using Microsoft.CodeAnalysis.MSBuild; - -using Spectre.Console; - -namespace KubeOps.Cli.Roslyn; - -/// -/// AssemblyParser. -/// -internal sealed partial class AssemblyParser -{ - private readonly Assembly[] _assemblies; - - static AssemblyParser() - { - MSBuildLocator.RegisterDefaults(); - } - - private AssemblyParser(params Assembly[] assemblies) => _assemblies = assemblies; - - 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(); - return new AssemblyParser(Assembly.Load(assemblyStream.ToArray())); - }); - - 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.Load(assemblyStream.ToArray()); - })); - - console.WriteLine(); - return new AssemblyParser(assemblies); - }); - - public IEnumerable Entities() => - _assemblies - .SelectMany(a => a.DefinedTypes) - .Where(t => t.GetCustomAttributes().Any()) - .Where(type => !type.GetCustomAttributes().Any()); - - public IEnumerable RbacAttributes() - { - foreach (var type in _assemblies - .SelectMany(a => a.DefinedTypes) - .SelectMany(t => - t.GetCustomAttributes())) - { - yield return type; - } - - foreach (var type in _assemblies - .SelectMany(a => a.DefinedTypes) - .SelectMany(t => - t.GetCustomAttributes())) - { - yield return type; - } - } - - [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 Microsoft.Build.Locator; +using Microsoft.CodeAnalysis.MSBuild; + +using Spectre.Console; + +namespace KubeOps.Cli.Roslyn; + +/// +/// AssemblyParser. +/// +internal sealed partial class AssemblyParser +{ + private readonly Assembly[] _assemblies; + + static AssemblyParser() + { + MSBuildLocator.RegisterDefaults(); + } + + private AssemblyParser(params Assembly[] assemblies) => _assemblies = assemblies; + + 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(); + return new AssemblyParser(Assembly.Load(assemblyStream.ToArray())); + }); + + 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.Load(assemblyStream.ToArray()); + })); + + console.WriteLine(); + return new AssemblyParser(assemblies); + }); + + public IEnumerable Entities() => + _assemblies + .SelectMany(a => a.DefinedTypes) + .Where(t => t.GetCustomAttributes().Any()) + .Where(type => !type.GetCustomAttributes().Any()); + + public IEnumerable RbacAttributes() + { + foreach (var type in _assemblies + .SelectMany(a => a.DefinedTypes) + .SelectMany(t => + t.GetCustomAttributes())) + { + yield return type; + } + + foreach (var type in _assemblies + .SelectMany(a => a.DefinedTypes) + .SelectMany(t => + t.GetCustomAttributes())) + { + yield return type; + } + } + + [GeneratedRegex(".*")] + private static partial Regex DefaultRegex(); +} diff --git a/src/KubeOps.Cli/Roslyn/TfmComparer.cs b/src/KubeOps.Cli/Roslyn/TfmComparer.cs index 37bec917..77e8ef67 100644 --- a/src/KubeOps.Cli/Roslyn/TfmComparer.cs +++ b/src/KubeOps.Cli/Roslyn/TfmComparer.cs @@ -1,58 +1,58 @@ -using System.Text.RegularExpressions; - -namespace KubeOps.Cli.Roslyn; - -/// -/// 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.Roslyn; + +/// +/// 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.Generator/Generators/ControllerRegistrationGenerator.cs b/src/KubeOps.Generator/Generators/ControllerRegistrationGenerator.cs new file mode 100644 index 00000000..526d6d67 --- /dev/null +++ b/src/KubeOps.Generator/Generators/ControllerRegistrationGenerator.cs @@ -0,0 +1,85 @@ +using System.Text; + +using KubeOps.Generator.SyntaxReceiver; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace KubeOps.Generator.Generators; + +[Generator] +public class ControllerRegistrationGenerator : ISourceGenerator +{ + private readonly EntityControllerSyntaxReceiver _ctrlReceiver = new(); + private readonly KubernetesEntitySyntaxReceiver _entityReceiver = new(); + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new CombinedSyntaxReceiver(_ctrlReceiver, _entityReceiver)); + } + + public void Execute(GeneratorExecutionContext context) + { + if (context.SyntaxContextReceiver is not CombinedSyntaxReceiver) + { + return; + } + + var declaration = CompilationUnit() + .WithUsings( + List( + new List { UsingDirective(IdentifierName("KubeOps.Abstractions.Builder")), })) + .WithMembers(SingletonList(ClassDeclaration("ControllerRegistrations") + .WithModifiers(TokenList( + Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) + .AddMembers(MethodDeclaration(IdentifierName("IOperatorBuilder"), "RegisterControllers") + .WithModifiers( + TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) + .WithParameterList(ParameterList( + SingletonSeparatedList( + Parameter( + Identifier("builder")) + .WithModifiers( + TokenList( + Token(SyntaxKind.ThisKeyword))) + .WithType( + IdentifierName("IOperatorBuilder"))))) + .WithBody(Block( + _ctrlReceiver.Controllers + .Where(c => _entityReceiver.Entities.Exists(e => + e.Class.Identifier.ToString() == c.EntityName)) + .Select(c => (c.Controller, Entity: _entityReceiver.Entities.First(e => + e.Class.Identifier.ToString() == c.EntityName).Class)) + .Select(e => ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("builder"), + GenericName(Identifier("AddController")) + .WithTypeArgumentList( + TypeArgumentList( + SeparatedList(new[] + { + IdentifierName(context.Compilation + .GetSemanticModel(e.Controller.SyntaxTree) + .GetDeclaredSymbol(e.Controller)! + .ToDisplayString(SymbolDisplayFormat + .FullyQualifiedFormat)), + IdentifierName(context.Compilation + .GetSemanticModel(e.Entity.SyntaxTree) + .GetDeclaredSymbol(e.Entity)! + .ToDisplayString(SymbolDisplayFormat + .FullyQualifiedFormat)), + }))))))) + .Append(ReturnStatement(IdentifierName("builder")))))))) + .NormalizeWhitespace(); + + context.AddSource( + "ControllerRegistrations.g.cs", + SourceText.From(declaration.ToString(), Encoding.UTF8, SourceHashAlgorithm.Sha256)); + } +} diff --git a/src/KubeOps.Generator/EntityDefinitions/EntityDefinitionGenerator.cs b/src/KubeOps.Generator/Generators/EntityDefinitionGenerator.cs similarity index 94% rename from src/KubeOps.Generator/EntityDefinitions/EntityDefinitionGenerator.cs rename to src/KubeOps.Generator/Generators/EntityDefinitionGenerator.cs index 240b7a7e..d7158312 100644 --- a/src/KubeOps.Generator/EntityDefinitions/EntityDefinitionGenerator.cs +++ b/src/KubeOps.Generator/Generators/EntityDefinitionGenerator.cs @@ -1,12 +1,15 @@ +using System.Text; + using KubeOps.Generator.SyntaxReceiver; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -namespace KubeOps.Generator.EntityDefinitions; +namespace KubeOps.Generator.Generators; [Generator] public class EntityDefinitionGenerator : ISourceGenerator @@ -76,7 +79,7 @@ public void Execute(GeneratorExecutionContext context) Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.ReadOnlyKeyword)))))) - .AddMembers(MethodDeclaration(IdentifierName("IOperatorBuilder"), "RegisterEntitiyMetadata") + .AddMembers(MethodDeclaration(IdentifierName("IOperatorBuilder"), "RegisterEntities") .WithModifiers( TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) .WithParameterList(ParameterList( @@ -112,6 +115,8 @@ public void Execute(GeneratorExecutionContext context) .Append(ReturnStatement(IdentifierName("builder")))))))) .NormalizeWhitespace(); - context.AddSource("EntityDefinitions.g.cs", $"// \n\n{declaration}"); + context.AddSource( + "EntityDefinitions.g.cs", + SourceText.From(declaration.ToString(), Encoding.UTF8, SourceHashAlgorithm.Sha256)); } } diff --git a/src/KubeOps.Generator/Generators/OperatorBuilderGenerator.cs b/src/KubeOps.Generator/Generators/OperatorBuilderGenerator.cs new file mode 100644 index 00000000..d9b1d822 --- /dev/null +++ b/src/KubeOps.Generator/Generators/OperatorBuilderGenerator.cs @@ -0,0 +1,62 @@ +using System.Text; + +using KubeOps.Generator.SyntaxReceiver; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace KubeOps.Generator.Generators; + +[Generator] +public class OperatorBuilderGenerator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { + } + + public void Execute(GeneratorExecutionContext context) + { + var declaration = CompilationUnit() + .WithUsings( + List( + new List { UsingDirective(IdentifierName("KubeOps.Abstractions.Builder")), })) + .WithMembers(SingletonList(ClassDeclaration("OperatorBuilderExtensions") + .WithModifiers(TokenList( + Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) + .AddMembers(MethodDeclaration(IdentifierName("IOperatorBuilder"), "RegisterResources") + .WithModifiers( + TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) + .WithParameterList(ParameterList( + SingletonSeparatedList( + Parameter( + Identifier("builder")) + .WithModifiers( + TokenList( + Token(SyntaxKind.ThisKeyword))) + .WithType( + IdentifierName("IOperatorBuilder"))))) + .WithBody(Block( + ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("builder"), + IdentifierName("RegisterEntities")))), + ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("builder"), + IdentifierName("RegisterControllers")))), + ReturnStatement(IdentifierName("builder"))))))) + .NormalizeWhitespace(); + + context.AddSource( + "OperatorBuilder.g.cs", + SourceText.From(declaration.ToString(), Encoding.UTF8, SourceHashAlgorithm.Sha256)); + } +} diff --git a/src/KubeOps.Generator/SyntaxReceiver/AttributedEntity.cs b/src/KubeOps.Generator/SyntaxReceiver/AttributedEntity.cs index 58674eff..9d1ed7d5 100644 --- a/src/KubeOps.Generator/SyntaxReceiver/AttributedEntity.cs +++ b/src/KubeOps.Generator/SyntaxReceiver/AttributedEntity.cs @@ -1,10 +1,10 @@ -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace KubeOps.Generator.SyntaxReceiver; - -public record struct AttributedEntity( - ClassDeclarationSyntax Class, - string Kind, - string Version, - string? Group, - string? Plural); +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace KubeOps.Generator.SyntaxReceiver; + +public record struct AttributedEntity( + ClassDeclarationSyntax Class, + string Kind, + string Version, + string? Group, + string? Plural); diff --git a/src/KubeOps.Generator/SyntaxReceiver/CombinedSyntaxReceiver.cs b/src/KubeOps.Generator/SyntaxReceiver/CombinedSyntaxReceiver.cs new file mode 100644 index 00000000..e64f5ad2 --- /dev/null +++ b/src/KubeOps.Generator/SyntaxReceiver/CombinedSyntaxReceiver.cs @@ -0,0 +1,21 @@ +using Microsoft.CodeAnalysis; + +namespace KubeOps.Generator.SyntaxReceiver; + +public class CombinedSyntaxReceiver : ISyntaxContextReceiver +{ + private readonly ISyntaxContextReceiver[] _receivers; + + public CombinedSyntaxReceiver(params ISyntaxContextReceiver[] receivers) + { + _receivers = receivers; + } + + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + foreach (var syntaxContextReceiver in _receivers) + { + syntaxContextReceiver.OnVisitSyntaxNode(context); + } + } +} diff --git a/src/KubeOps.Generator/SyntaxReceiver/EntityControllerSyntaxReceiver.cs b/src/KubeOps.Generator/SyntaxReceiver/EntityControllerSyntaxReceiver.cs new file mode 100644 index 00000000..aa1f4867 --- /dev/null +++ b/src/KubeOps.Generator/SyntaxReceiver/EntityControllerSyntaxReceiver.cs @@ -0,0 +1,22 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace KubeOps.Generator.SyntaxReceiver; + +public class EntityControllerSyntaxReceiver : ISyntaxContextReceiver +{ + public List<(ClassDeclarationSyntax Controller, string EntityName)> Controllers { get; } = new(); + + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is not ClassDeclarationSyntax { BaseList.Types.Count: > 0 } cls || + cls.BaseList.Types.FirstOrDefault(t => t is + { Type: GenericNameSyntax { Identifier.Text: "IEntityController" } }) is not { } baseType) + { + return; + } + + var targetEntity = (baseType.Type as GenericNameSyntax)!.TypeArgumentList.Arguments.First(); + Controllers.Add((cls, targetEntity.ToString())); + } +} diff --git a/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs b/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs index 37cfe3ac..b56c5d4d 100644 --- a/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs +++ b/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs @@ -1,40 +1,38 @@ -using KubeOps.Generator.EntityDefinitions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace KubeOps.Generator.SyntaxReceiver; - -public class KubernetesEntitySyntaxReceiver : ISyntaxContextReceiver -{ - private const string KindName = "Kind"; - private const string GroupName = "Group"; - private const string PluralName = "Plural"; - private const string VersionName = "ApiVersion"; - private const string DefaultVersion = "v1"; - - public List Entities { get; } = new(); - - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - if (context.Node is not ClassDeclarationSyntax { AttributeLists.Count: > 0 } cls || - cls.AttributeLists.SelectMany(a => a.Attributes) - .FirstOrDefault(a => a.Name.ToString() == "KubernetesEntity") is not { } attr) - { - return; - } - - Entities.Add(new( - cls, - GetArgumentValue(attr, KindName) ?? cls.Identifier.ToString(), - GetArgumentValue(attr, VersionName) ?? DefaultVersion, - GetArgumentValue(attr, GroupName), - GetArgumentValue(attr, PluralName))); - } - - private static string? GetArgumentValue(AttributeSyntax attr, string argName) => - attr.ArgumentList?.Arguments.FirstOrDefault(a => a.NameEquals?.Name.ToString() == argName) is - { Expression: LiteralExpressionSyntax { Token.ValueText: { } value } } - ? value - : null; -} +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace KubeOps.Generator.SyntaxReceiver; + +public class KubernetesEntitySyntaxReceiver : ISyntaxContextReceiver +{ + private const string KindName = "Kind"; + private const string GroupName = "Group"; + private const string PluralName = "Plural"; + private const string VersionName = "ApiVersion"; + private const string DefaultVersion = "v1"; + + public List Entities { get; } = new(); + + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is not ClassDeclarationSyntax { AttributeLists.Count: > 0 } cls || + cls.AttributeLists.SelectMany(a => a.Attributes) + .FirstOrDefault(a => a.Name.ToString() == "KubernetesEntity") is not { } attr) + { + return; + } + + Entities.Add(new( + cls, + GetArgumentValue(attr, KindName) ?? cls.Identifier.ToString(), + GetArgumentValue(attr, VersionName) ?? DefaultVersion, + GetArgumentValue(attr, GroupName), + GetArgumentValue(attr, PluralName))); + } + + private static string? GetArgumentValue(AttributeSyntax attr, string argName) => + attr.ArgumentList?.Arguments.FirstOrDefault(a => a.NameEquals?.Name.ToString() == argName) is + { Expression: LiteralExpressionSyntax { Token.ValueText: { } value } } + ? value + : null; +} diff --git a/src/KubeOps.KubernetesClient/IKubernetesClient.cs b/src/KubeOps.KubernetesClient/IKubernetesClient.cs index 68a966c5..940dc1d4 100644 --- a/src/KubeOps.KubernetesClient/IKubernetesClient.cs +++ b/src/KubeOps.KubernetesClient/IKubernetesClient.cs @@ -1,194 +1,194 @@ -using k8s; -using k8s.Models; - -using KubeOps.Abstractions.Entities; -using KubeOps.KubernetesClient.LabelSelectors; - -namespace KubeOps.KubernetesClient; - -/// -/// Client for the Kubernetes API. Contains various methods to manage Kubernetes entities. -/// This client is specific to an entity of type . -/// -/// The type of the Kubernetes entity. -public interface IKubernetesClient - where TEntity : IKubernetesObject -{ - /// - /// Return the base URI of the currently used KubernetesClient. - /// - Uri BaseUri { get; } - - /// - /// Returns the name of the current namespace. - /// To determine the current namespace the following places (in the given order) are checked: - /// - /// - /// The created Kubernetes configuration (from file / in-cluster) - /// - /// - /// - /// The env variable given as the param to the function (default "POD_NAMESPACE") - /// which can be provided by the Kubernetes downward API - /// - /// - /// - /// - /// The fallback secret file if running on the cluster - /// (/var/run/secrets/Kubernetes.io/serviceaccount/namespace) - /// - /// - /// - /// `default` - /// - /// - /// - /// Customizable name of the env var to check for the namespace. - /// A string containing the current namespace (or a fallback of it). - Task GetCurrentNamespace(string downwardApiEnvName = "POD_NAMESPACE"); - - /// - /// Fetch and return an entity from the Kubernetes API. - /// - /// The name of the entity (metadata.name). - /// - /// Optional namespace. If this is set, the entity must be a namespaced entity. - /// If it is omitted, the entity must be a cluster wide entity. - /// - /// The found entity of the given type, or null otherwise. - Task Get(string name, string? @namespace = null); - - /// - /// Fetch and return a list of entities from the Kubernetes API. - /// - /// If the entities are namespaced, provide the name of the namespace. - /// A string, representing an optional label selector for filtering fetched objects. - /// A list of Kubernetes entities. - Task> List( - string? @namespace = null, - string? labelSelector = null); - - /// - /// Fetch and return a list of entities from the Kubernetes API. - /// - /// - /// If only entities in a given namespace should be listed, provide the namespace here. - /// - /// A list of label-selectors to apply to the search. - /// A list of Kubernetes entities. - Task> List( - string? @namespace = null, - params LabelSelector[] labelSelectors); - - /// - /// Create or Update a entity. This first fetches the entity from the Kubernetes API - /// and if it does exist, updates the entity. Otherwise, the entity is created. - /// - /// The entity in question. - /// The saved instance of the entity. - async Task Save(TEntity entity) => await Get(entity.Name(), entity.Namespace()) switch - { - { } e => await Update(entity.WithResourceVersion(e)), - _ => await Create(entity), - }; - - /// - /// Create the given entity on the Kubernetes API. - /// - /// The entity instance. - /// The created instance of the entity. - Task Create(TEntity entity); - - /// - /// Update the given entity on the Kubernetes API. - /// - /// The entity instance. - /// The updated instance of the entity. - Task Update(TEntity entity); - - /// - /// Update the status object of a given entity on the Kubernetes API. - /// - /// The entity that contains a status object. - /// A task that completes when the call was made. - public Task UpdateStatus(TEntity entity); - - /// - /// Delete a given entity from the Kubernetes API. - /// - /// The entity in question. - /// A task that completes when the call was made. - Task Delete(TEntity entity); - - /// - /// Delete a given list of entities from the Kubernetes API. - /// - /// The entities in question. - /// A task that completes when the calls were made. - Task Delete(IEnumerable entities); - - /// - /// Delete a given list of entities from the Kubernetes API. - /// - /// The entities in question. - /// A task that completes when the calls were made. - Task Delete(params TEntity[] entities); - - /// - /// Delete a given entity by name from the Kubernetes API. - /// - /// The name of the entity. - /// The optional namespace of the entity. - /// A task that completes when the call was made. - Task Delete(string name, string? @namespace = null); - - /// - /// Create a entity watcher on the Kubernetes API. - /// The entity watcher fires events for entity-events on - /// Kubernetes (events: . - /// - /// Action that is called when an event occurs. - /// Action that handles exceptions. - /// Action that handles closed connections. - /// - /// The namespace to watch for entities (if needed). - /// If the namespace is omitted, all entities on the cluster are watched. - /// - /// The timeout which the watcher has (after this timeout, the server will close the connection). - /// Cancellation-Token. - /// A list of label-selectors to apply to the search. - /// A entity watcher for the given entity. - Watcher Watch( - Action onEvent, - Action? onError = null, - Action? onClose = null, - string? @namespace = null, - TimeSpan? timeout = null, - CancellationToken cancellationToken = default, - params LabelSelector[] labelSelectors); - - /// - /// Create a entity watcher on the Kubernetes API. - /// The entity watcher fires events for entity-events on - /// Kubernetes (events: . - /// - /// Action that is called when an event occurs. - /// Action that handles exceptions. - /// Action that handles closed connections. - /// - /// The namespace to watch for entities (if needed). - /// If the namespace is omitted, all entities on the cluster are watched. - /// - /// The timeout which the watcher has (after this timeout, the server will close the connection). - /// A string, representing an optional label selector for filtering watched objects. - /// Cancellation-Token. - /// A entity watcher for the given entity. - Watcher Watch( - Action onEvent, - Action? onError = null, - Action? onClose = null, - string? @namespace = null, - TimeSpan? timeout = null, - string? labelSelector = null, - CancellationToken cancellationToken = default); -} +using k8s; +using k8s.Models; + +using KubeOps.Abstractions.Entities; +using KubeOps.KubernetesClient.LabelSelectors; + +namespace KubeOps.KubernetesClient; + +/// +/// Client for the Kubernetes API. Contains various methods to manage Kubernetes entities. +/// This client is specific to an entity of type . +/// +/// The type of the Kubernetes entity. +public interface IKubernetesClient + where TEntity : IKubernetesObject +{ + /// + /// Return the base URI of the currently used KubernetesClient. + /// + Uri BaseUri { get; } + + /// + /// Returns the name of the current namespace. + /// To determine the current namespace the following places (in the given order) are checked: + /// + /// + /// The created Kubernetes configuration (from file / in-cluster) + /// + /// + /// + /// The env variable given as the param to the function (default "POD_NAMESPACE") + /// which can be provided by the Kubernetes downward API + /// + /// + /// + /// + /// The fallback secret file if running on the cluster + /// (/var/run/secrets/Kubernetes.io/serviceaccount/namespace) + /// + /// + /// + /// `default` + /// + /// + /// + /// Customizable name of the env var to check for the namespace. + /// A string containing the current namespace (or a fallback of it). + Task GetCurrentNamespace(string downwardApiEnvName = "POD_NAMESPACE"); + + /// + /// Fetch and return an entity from the Kubernetes API. + /// + /// The name of the entity (metadata.name). + /// + /// Optional namespace. If this is set, the entity must be a namespaced entity. + /// If it is omitted, the entity must be a cluster wide entity. + /// + /// The found entity of the given type, or null otherwise. + Task Get(string name, string? @namespace = null); + + /// + /// Fetch and return a list of entities from the Kubernetes API. + /// + /// If the entities are namespaced, provide the name of the namespace. + /// A string, representing an optional label selector for filtering fetched objects. + /// A list of Kubernetes entities. + Task> List( + string? @namespace = null, + string? labelSelector = null); + + /// + /// Fetch and return a list of entities from the Kubernetes API. + /// + /// + /// If only entities in a given namespace should be listed, provide the namespace here. + /// + /// A list of label-selectors to apply to the search. + /// A list of Kubernetes entities. + Task> List( + string? @namespace = null, + params LabelSelector[] labelSelectors); + + /// + /// Create or Update a entity. This first fetches the entity from the Kubernetes API + /// and if it does exist, updates the entity. Otherwise, the entity is created. + /// + /// The entity in question. + /// The saved instance of the entity. + async Task Save(TEntity entity) => await Get(entity.Name(), entity.Namespace()) switch + { + { } e => await Update(entity.WithResourceVersion(e)), + _ => await Create(entity), + }; + + /// + /// Create the given entity on the Kubernetes API. + /// + /// The entity instance. + /// The created instance of the entity. + Task Create(TEntity entity); + + /// + /// Update the given entity on the Kubernetes API. + /// + /// The entity instance. + /// The updated instance of the entity. + Task Update(TEntity entity); + + /// + /// Update the status object of a given entity on the Kubernetes API. + /// + /// The entity that contains a status object. + /// A task that completes when the call was made. + public Task UpdateStatus(TEntity entity); + + /// + /// Delete a given entity from the Kubernetes API. + /// + /// The entity in question. + /// A task that completes when the call was made. + Task Delete(TEntity entity); + + /// + /// Delete a given list of entities from the Kubernetes API. + /// + /// The entities in question. + /// A task that completes when the calls were made. + Task Delete(IEnumerable entities); + + /// + /// Delete a given list of entities from the Kubernetes API. + /// + /// The entities in question. + /// A task that completes when the calls were made. + Task Delete(params TEntity[] entities); + + /// + /// Delete a given entity by name from the Kubernetes API. + /// + /// The name of the entity. + /// The optional namespace of the entity. + /// A task that completes when the call was made. + Task Delete(string name, string? @namespace = null); + + /// + /// Create a entity watcher on the Kubernetes API. + /// The entity watcher fires events for entity-events on + /// Kubernetes (events: . + /// + /// Action that is called when an event occurs. + /// Action that handles exceptions. + /// Action that handles closed connections. + /// + /// The namespace to watch for entities (if needed). + /// If the namespace is omitted, all entities on the cluster are watched. + /// + /// The timeout which the watcher has (after this timeout, the server will close the connection). + /// Cancellation-Token. + /// A list of label-selectors to apply to the search. + /// A entity watcher for the given entity. + Watcher Watch( + Action onEvent, + Action? onError = null, + Action? onClose = null, + string? @namespace = null, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default, + params LabelSelector[] labelSelectors); + + /// + /// Create a entity watcher on the Kubernetes API. + /// The entity watcher fires events for entity-events on + /// Kubernetes (events: . + /// + /// Action that is called when an event occurs. + /// Action that handles exceptions. + /// Action that handles closed connections. + /// + /// The namespace to watch for entities (if needed). + /// If the namespace is omitted, all entities on the cluster are watched. + /// + /// The timeout which the watcher has (after this timeout, the server will close the connection). + /// A string, representing an optional label selector for filtering watched objects. + /// Cancellation-Token. + /// A entity watcher for the given entity. + Watcher Watch( + Action onEvent, + Action? onError = null, + Action? onClose = null, + string? @namespace = null, + TimeSpan? timeout = null, + string? labelSelector = null, + CancellationToken cancellationToken = default); +} diff --git a/src/KubeOps.KubernetesClient/KubernetesClient.cs b/src/KubeOps.KubernetesClient/KubernetesClient.cs index 7c16ff63..faefb8f6 100644 --- a/src/KubeOps.KubernetesClient/KubernetesClient.cs +++ b/src/KubeOps.KubernetesClient/KubernetesClient.cs @@ -1,281 +1,281 @@ -using System.Net; - -using k8s; -using k8s.Autorest; -using k8s.Models; - -using KubeOps.Abstractions.Entities; -using KubeOps.KubernetesClient.LabelSelectors; - -namespace KubeOps.KubernetesClient; - -/// -public class KubernetesClient : IKubernetesClient, IDisposable - where TEntity : IKubernetesObject -{ - private const string DownwardApiNamespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"; - private const string DefaultNamespace = "default"; - - private readonly EntityMetadata _metadata; - private readonly KubernetesClientConfiguration _clientConfig; - private readonly IKubernetes _client; - private readonly GenericClient _genericClient; - - /// - /// Create a new Kubernetes client for the given entity. - /// The client will use the default configuration. - /// - /// The metadata of the entity. - public KubernetesClient(EntityMetadata metadata) - : this(metadata, KubernetesClientConfiguration.BuildDefaultConfig()) - { - } - - /// - /// Create a new Kubernetes client for the given entity with a custom client configuration. - /// - /// The metadata of the entity. - /// The config for the underlying Kubernetes client. - public KubernetesClient(EntityMetadata metadata, KubernetesClientConfiguration clientConfig) - : this(metadata, clientConfig, new Kubernetes(clientConfig)) - { - } - - /// - /// Create a new Kubernetes client for the given entity with a custom client configuration and client. - /// - /// The metadata of the entity. - /// The config for the underlying Kubernetes client. - /// The underlying client. - public KubernetesClient(EntityMetadata metadata, KubernetesClientConfiguration clientConfig, IKubernetes client) - { - _metadata = metadata; - _clientConfig = clientConfig; - _client = client; - _genericClient = metadata.Group switch - { - null => new GenericClient( - client, - metadata.Version, - metadata.PluralName, - false), - _ => new GenericClient( - client, - metadata.Group, - metadata.Version, - metadata.PluralName, - false), - }; - } - - /// - public Uri BaseUri => _client.BaseUri; - - /// - public async Task GetCurrentNamespace(string downwardApiEnvName = "POD_NAMESPACE") - { - if (_clientConfig.Namespace is { } configValue) - { - return configValue; - } - - if (Environment.GetEnvironmentVariable(downwardApiEnvName) is { } envValue) - { - return envValue; - } - - if (File.Exists(DownwardApiNamespaceFile)) - { - var ns = await File.ReadAllTextAsync(DownwardApiNamespaceFile); - return ns.Trim(); - } - - return DefaultNamespace; - } - - /// - public async Task Get(string name, string? @namespace = null) - { - var list = @namespace switch - { - null => await _client.CustomObjects.ListClusterCustomObjectAsync>( - _metadata.Group ?? string.Empty, - _metadata.Version, - _metadata.PluralName), - _ => await _client.CustomObjects.ListNamespacedCustomObjectAsync>( - _metadata.Group ?? string.Empty, - _metadata.Version, - @namespace, - _metadata.PluralName), - }; - - return list switch - { - { Items: [var existing] } => existing, - _ => default, - }; - } - - /// - public async Task> List(string? @namespace = null, string? labelSelector = null) - => (@namespace switch - { - null => await _client.CustomObjects.ListClusterCustomObjectAsync>( - _metadata.Group ?? string.Empty, - _metadata.Version, - _metadata.PluralName, - labelSelector: labelSelector), - _ => await _client.CustomObjects.ListNamespacedCustomObjectAsync>( - _metadata.Group ?? string.Empty, - _metadata.Version, - @namespace, - _metadata.PluralName, - labelSelector: labelSelector), - }).Items; - - /// - public Task> List(string? @namespace = null, params LabelSelector[] labelSelectors) - => List(@namespace, labelSelectors.ToExpression()); - - /// - public Task Create(TEntity entity) - => entity.Namespace() switch - { - { } ns => _genericClient.CreateNamespacedAsync(entity, ns), - null => _genericClient.CreateAsync(entity), - }; - - /// - public Task Update(TEntity entity) - => entity.Namespace() switch - { - { } ns => _genericClient.ReplaceNamespacedAsync(entity, ns, entity.Name()), - null => _genericClient.ReplaceAsync(entity, entity.Name()), - }; - - /// - public Task UpdateStatus(TEntity entity) - => entity.Namespace() switch - { - { } ns => _client.CustomObjects.ReplaceNamespacedCustomObjectStatusAsync( - entity, - _metadata.Group ?? string.Empty, - _metadata.Version, - ns, - _metadata.PluralName, - entity.Name()), - _ => _client.CustomObjects.ReplaceClusterCustomObjectStatusAsync( - entity, - _metadata.Group ?? string.Empty, - _metadata.Version, - _metadata.PluralName, - entity.Name()), - }; - - /// - public Task Delete(TEntity entity) => Delete( - entity.Name(), - entity.Namespace()); - - /// - public Task Delete(IEnumerable entities) => - Task.WhenAll(entities.Select(Delete)); - - /// - public Task Delete(params TEntity[] entities) => - Task.WhenAll(entities.Select(Delete)); - - /// - public async Task Delete(string name, string? @namespace = null) - { - try - { - switch (@namespace) - { - case not null: - await _genericClient.DeleteNamespacedAsync(@namespace, name); - break; - default: - await _genericClient.DeleteAsync(name); - break; - } - } - catch (HttpOperationException e) when (e.Response.StatusCode == HttpStatusCode.NotFound) - { - // The resource was not found. We can ignore this. - } - } - - /// - public Watcher Watch( - Action onEvent, - Action? onError = null, - Action? onClose = null, - string? @namespace = null, - TimeSpan? timeout = null, - CancellationToken cancellationToken = default, - params LabelSelector[] labelSelectors) - => Watch( - onEvent, - onError, - onClose, - @namespace, - timeout, - labelSelectors.ToExpression(), - cancellationToken); - - /// - public Watcher Watch( - Action onEvent, - Action? onError = null, - Action? onClose = null, - string? @namespace = null, - TimeSpan? timeout = null, - string? labelSelector = null, - CancellationToken cancellationToken = default) - => (@namespace switch - { - not null => _client.CustomObjects.ListNamespacedCustomObjectWithHttpMessagesAsync( - _metadata.Group ?? string.Empty, - _metadata.Version, - @namespace, - _metadata.PluralName, - labelSelector: labelSelector, - timeoutSeconds: timeout switch - { - null => null, - _ => (int?)timeout.Value.TotalSeconds, - }, - watch: true, - cancellationToken: cancellationToken), - _ => _client.CustomObjects.ListClusterCustomObjectWithHttpMessagesAsync( - _metadata.Group ?? string.Empty, - _metadata.Version, - _metadata.PluralName, - labelSelector: labelSelector, - timeoutSeconds: timeout switch - { - null => null, - _ => (int?)timeout.Value.TotalSeconds, - }, - watch: true, - cancellationToken: cancellationToken), - }).Watch(onEvent, onError, onClose); - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposing) - { - return; - } - - _client.Dispose(); - _genericClient.Dispose(); - } -} +using System.Net; + +using k8s; +using k8s.Autorest; +using k8s.Models; + +using KubeOps.Abstractions.Entities; +using KubeOps.KubernetesClient.LabelSelectors; + +namespace KubeOps.KubernetesClient; + +/// +public class KubernetesClient : IKubernetesClient, IDisposable + where TEntity : IKubernetesObject +{ + private const string DownwardApiNamespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"; + private const string DefaultNamespace = "default"; + + private readonly EntityMetadata _metadata; + private readonly KubernetesClientConfiguration _clientConfig; + private readonly IKubernetes _client; + private readonly GenericClient _genericClient; + + /// + /// Create a new Kubernetes client for the given entity. + /// The client will use the default configuration. + /// + /// The metadata of the entity. + public KubernetesClient(EntityMetadata metadata) + : this(metadata, KubernetesClientConfiguration.BuildDefaultConfig()) + { + } + + /// + /// Create a new Kubernetes client for the given entity with a custom client configuration. + /// + /// The metadata of the entity. + /// The config for the underlying Kubernetes client. + public KubernetesClient(EntityMetadata metadata, KubernetesClientConfiguration clientConfig) + : this(metadata, clientConfig, new Kubernetes(clientConfig)) + { + } + + /// + /// Create a new Kubernetes client for the given entity with a custom client configuration and client. + /// + /// The metadata of the entity. + /// The config for the underlying Kubernetes client. + /// The underlying client. + public KubernetesClient(EntityMetadata metadata, KubernetesClientConfiguration clientConfig, IKubernetes client) + { + _metadata = metadata; + _clientConfig = clientConfig; + _client = client; + _genericClient = metadata.Group switch + { + null => new GenericClient( + client, + metadata.Version, + metadata.PluralName, + false), + _ => new GenericClient( + client, + metadata.Group, + metadata.Version, + metadata.PluralName, + false), + }; + } + + /// + public Uri BaseUri => _client.BaseUri; + + /// + public async Task GetCurrentNamespace(string downwardApiEnvName = "POD_NAMESPACE") + { + if (_clientConfig.Namespace is { } configValue) + { + return configValue; + } + + if (Environment.GetEnvironmentVariable(downwardApiEnvName) is { } envValue) + { + return envValue; + } + + if (File.Exists(DownwardApiNamespaceFile)) + { + var ns = await File.ReadAllTextAsync(DownwardApiNamespaceFile); + return ns.Trim(); + } + + return DefaultNamespace; + } + + /// + public async Task Get(string name, string? @namespace = null) + { + var list = @namespace switch + { + null => await _client.CustomObjects.ListClusterCustomObjectAsync>( + _metadata.Group ?? string.Empty, + _metadata.Version, + _metadata.PluralName), + _ => await _client.CustomObjects.ListNamespacedCustomObjectAsync>( + _metadata.Group ?? string.Empty, + _metadata.Version, + @namespace, + _metadata.PluralName), + }; + + return list switch + { + { Items: [var existing] } => existing, + _ => default, + }; + } + + /// + public async Task> List(string? @namespace = null, string? labelSelector = null) + => (@namespace switch + { + null => await _client.CustomObjects.ListClusterCustomObjectAsync>( + _metadata.Group ?? string.Empty, + _metadata.Version, + _metadata.PluralName, + labelSelector: labelSelector), + _ => await _client.CustomObjects.ListNamespacedCustomObjectAsync>( + _metadata.Group ?? string.Empty, + _metadata.Version, + @namespace, + _metadata.PluralName, + labelSelector: labelSelector), + }).Items; + + /// + public Task> List(string? @namespace = null, params LabelSelector[] labelSelectors) + => List(@namespace, labelSelectors.ToExpression()); + + /// + public Task Create(TEntity entity) + => entity.Namespace() switch + { + { } ns => _genericClient.CreateNamespacedAsync(entity, ns), + null => _genericClient.CreateAsync(entity), + }; + + /// + public Task Update(TEntity entity) + => entity.Namespace() switch + { + { } ns => _genericClient.ReplaceNamespacedAsync(entity, ns, entity.Name()), + null => _genericClient.ReplaceAsync(entity, entity.Name()), + }; + + /// + public Task UpdateStatus(TEntity entity) + => entity.Namespace() switch + { + { } ns => _client.CustomObjects.ReplaceNamespacedCustomObjectStatusAsync( + entity, + _metadata.Group ?? string.Empty, + _metadata.Version, + ns, + _metadata.PluralName, + entity.Name()), + _ => _client.CustomObjects.ReplaceClusterCustomObjectStatusAsync( + entity, + _metadata.Group ?? string.Empty, + _metadata.Version, + _metadata.PluralName, + entity.Name()), + }; + + /// + public Task Delete(TEntity entity) => Delete( + entity.Name(), + entity.Namespace()); + + /// + public Task Delete(IEnumerable entities) => + Task.WhenAll(entities.Select(Delete)); + + /// + public Task Delete(params TEntity[] entities) => + Task.WhenAll(entities.Select(Delete)); + + /// + public async Task Delete(string name, string? @namespace = null) + { + try + { + switch (@namespace) + { + case not null: + await _genericClient.DeleteNamespacedAsync(@namespace, name); + break; + default: + await _genericClient.DeleteAsync(name); + break; + } + } + catch (HttpOperationException e) when (e.Response.StatusCode == HttpStatusCode.NotFound) + { + // The resource was not found. We can ignore this. + } + } + + /// + public Watcher Watch( + Action onEvent, + Action? onError = null, + Action? onClose = null, + string? @namespace = null, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default, + params LabelSelector[] labelSelectors) + => Watch( + onEvent, + onError, + onClose, + @namespace, + timeout, + labelSelectors.ToExpression(), + cancellationToken); + + /// + public Watcher Watch( + Action onEvent, + Action? onError = null, + Action? onClose = null, + string? @namespace = null, + TimeSpan? timeout = null, + string? labelSelector = null, + CancellationToken cancellationToken = default) + => (@namespace switch + { + not null => _client.CustomObjects.ListNamespacedCustomObjectWithHttpMessagesAsync( + _metadata.Group ?? string.Empty, + _metadata.Version, + @namespace, + _metadata.PluralName, + labelSelector: labelSelector, + timeoutSeconds: timeout switch + { + null => null, + _ => (int?)timeout.Value.TotalSeconds, + }, + watch: true, + cancellationToken: cancellationToken), + _ => _client.CustomObjects.ListClusterCustomObjectWithHttpMessagesAsync( + _metadata.Group ?? string.Empty, + _metadata.Version, + _metadata.PluralName, + labelSelector: labelSelector, + timeoutSeconds: timeout switch + { + null => null, + _ => (int?)timeout.Value.TotalSeconds, + }, + watch: true, + cancellationToken: cancellationToken), + }).Watch(onEvent, onError, onClose); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + _client.Dispose(); + _genericClient.Dispose(); + } +} diff --git a/src/KubeOps.KubernetesClient/LabelSelectors/EqualsSelector.cs b/src/KubeOps.KubernetesClient/LabelSelectors/EqualsSelector.cs index 0e313b2f..1751df6b 100644 --- a/src/KubeOps.KubernetesClient/LabelSelectors/EqualsSelector.cs +++ b/src/KubeOps.KubernetesClient/LabelSelectors/EqualsSelector.cs @@ -1,13 +1,13 @@ -namespace KubeOps.KubernetesClient.LabelSelectors; - -/// -/// Label-selector that checks if a certain label contains -/// a specific value (out of a list of values). -/// Note that "label in (value)" is the same as "label == value". -/// -/// The label that needs to equal to one of the values. -/// The possible values. -public record EqualsSelector(string Label, params string[] Values) : LabelSelector -{ - protected override string ToExpression() => $"{Label} in ({string.Join(",", Values)})"; -} +namespace KubeOps.KubernetesClient.LabelSelectors; + +/// +/// Label-selector that checks if a certain label contains +/// a specific value (out of a list of values). +/// Note that "label in (value)" is the same as "label == value". +/// +/// The label that needs to equal to one of the values. +/// The possible values. +public record EqualsSelector(string Label, params string[] Values) : LabelSelector +{ + protected override string ToExpression() => $"{Label} in ({string.Join(",", Values)})"; +} diff --git a/src/KubeOps.KubernetesClient/LabelSelectors/ExistsSelector.cs b/src/KubeOps.KubernetesClient/LabelSelectors/ExistsSelector.cs index ed40e73c..dccbfb1c 100644 --- a/src/KubeOps.KubernetesClient/LabelSelectors/ExistsSelector.cs +++ b/src/KubeOps.KubernetesClient/LabelSelectors/ExistsSelector.cs @@ -1,10 +1,10 @@ -namespace KubeOps.KubernetesClient.LabelSelectors; - -/// -/// Selector that checks if a certain label exists. -/// -/// The label that needs to exist on the entity/resource. -public record ExistsSelector(string Label) : LabelSelector -{ - protected override string ToExpression() => $"{Label}"; -} +namespace KubeOps.KubernetesClient.LabelSelectors; + +/// +/// Selector that checks if a certain label exists. +/// +/// The label that needs to exist on the entity/resource. +public record ExistsSelector(string Label) : LabelSelector +{ + protected override string ToExpression() => $"{Label}"; +} diff --git a/src/KubeOps.KubernetesClient/LabelSelectors/Extensions.cs b/src/KubeOps.KubernetesClient/LabelSelectors/Extensions.cs index f22e7b63..78b36276 100644 --- a/src/KubeOps.KubernetesClient/LabelSelectors/Extensions.cs +++ b/src/KubeOps.KubernetesClient/LabelSelectors/Extensions.cs @@ -1,12 +1,12 @@ -namespace KubeOps.KubernetesClient.LabelSelectors; - -public static class Extensions -{ - /// - /// Convert an enumerable list of s to a string. - /// - /// The list of selectors. - /// A comma-joined string with all selectors converted to their expressions. - public static string ToExpression(this IEnumerable selectors) => - string.Join(",", selectors); -} +namespace KubeOps.KubernetesClient.LabelSelectors; + +public static class Extensions +{ + /// + /// Convert an enumerable list of s to a string. + /// + /// The list of selectors. + /// A comma-joined string with all selectors converted to their expressions. + public static string ToExpression(this IEnumerable selectors) => + string.Join(",", selectors); +} diff --git a/src/KubeOps.KubernetesClient/LabelSelectors/LabelSelector.cs b/src/KubeOps.KubernetesClient/LabelSelectors/LabelSelector.cs index 543b1c03..2a49926e 100644 --- a/src/KubeOps.KubernetesClient/LabelSelectors/LabelSelector.cs +++ b/src/KubeOps.KubernetesClient/LabelSelectors/LabelSelector.cs @@ -1,20 +1,20 @@ -namespace KubeOps.KubernetesClient.LabelSelectors; - -/// -/// Different label selectors for querying the Kubernetes API. -/// -public abstract record LabelSelector -{ - /// - /// Cast the label selector to a string. - /// - /// The selector. - /// A string representation of the label selector. - public static implicit operator string(LabelSelector selector) => selector.ToExpression(); - - /// - /// Create an expression from the label selector. - /// - /// A string that represents the label selector. - protected abstract string ToExpression(); -} +namespace KubeOps.KubernetesClient.LabelSelectors; + +/// +/// Different label selectors for querying the Kubernetes API. +/// +public abstract record LabelSelector +{ + /// + /// Cast the label selector to a string. + /// + /// The selector. + /// A string representation of the label selector. + public static implicit operator string(LabelSelector selector) => selector.ToExpression(); + + /// + /// Create an expression from the label selector. + /// + /// A string that represents the label selector. + protected abstract string ToExpression(); +} diff --git a/src/KubeOps.KubernetesClient/LabelSelectors/NotEqualsSelector.cs b/src/KubeOps.KubernetesClient/LabelSelectors/NotEqualsSelector.cs index 32a68d09..cd461e36 100644 --- a/src/KubeOps.KubernetesClient/LabelSelectors/NotEqualsSelector.cs +++ b/src/KubeOps.KubernetesClient/LabelSelectors/NotEqualsSelector.cs @@ -1,13 +1,13 @@ -namespace KubeOps.KubernetesClient.LabelSelectors; - -/// -/// Label-selector that checks if a certain label does not contain -/// a specific value (out of a list of values). -/// Note that "label notin (value)" is the same as "label != value". -/// -/// The label that must not equal to one of the values. -/// The possible values. -public record NotEqualsSelector(string Label, params string[] Values) : LabelSelector -{ - protected override string ToExpression() => $"{Label} notin ({string.Join(",", Values)})"; -} +namespace KubeOps.KubernetesClient.LabelSelectors; + +/// +/// Label-selector that checks if a certain label does not contain +/// a specific value (out of a list of values). +/// Note that "label notin (value)" is the same as "label != value". +/// +/// The label that must not equal to one of the values. +/// The possible values. +public record NotEqualsSelector(string Label, params string[] Values) : LabelSelector +{ + protected override string ToExpression() => $"{Label} notin ({string.Join(",", Values)})"; +} diff --git a/src/KubeOps.KubernetesClient/LabelSelectors/NotExistsSelector.cs b/src/KubeOps.KubernetesClient/LabelSelectors/NotExistsSelector.cs index 52db320a..bebc6ef1 100644 --- a/src/KubeOps.KubernetesClient/LabelSelectors/NotExistsSelector.cs +++ b/src/KubeOps.KubernetesClient/LabelSelectors/NotExistsSelector.cs @@ -1,10 +1,10 @@ -namespace KubeOps.KubernetesClient.LabelSelectors; - -/// -/// Selector that checks if a certain label does not exist. -/// -/// The label that must not exist on the entity/resource. -public record NotExistsSelector(string Label) : LabelSelector -{ - protected override string ToExpression() => $"!{Label}"; -} +namespace KubeOps.KubernetesClient.LabelSelectors; + +/// +/// Selector that checks if a certain label does not exist. +/// +/// The label that must not exist on the entity/resource. +public record NotExistsSelector(string Label) : LabelSelector +{ + protected override string ToExpression() => $"!{Label}"; +} diff --git a/src/KubeOps.Transpiler/Crds.cs b/src/KubeOps.Transpiler/Crds.cs index 18f25276..af3e1792 100644 --- a/src/KubeOps.Transpiler/Crds.cs +++ b/src/KubeOps.Transpiler/Crds.cs @@ -1,562 +1,562 @@ -using System.Collections; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Text.Json.Serialization; -using System.Text.RegularExpressions; - -using k8s; -using k8s.Models; - -using KubeOps.Abstractions.Entities; -using KubeOps.Abstractions.Entities.Attributes; - -using Namotion.Reflection; - -namespace KubeOps.Transpiler; - -/// -/// Class for the conversion of C# types to Kubernetes CRDs. -/// -public static partial 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" }; - - /// - /// Transpiles the given Kubernetes entity type to a object. - /// - /// The Kubernetes entity type to transpile. - /// A object representing the transpiled entity type. - /// Thrown if the given type is not a valid Kubernetes entity. - public static V1CustomResourceDefinition Transpile(Type type) - { - var (meta, scope) = Entities.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, - ShortNames = type.GetCustomAttributes(true) - .SelectMany(a => a.ShortNames).ToList() switch - { - { Count: > 0 } p => p, - _ => null, - }, - }; - crd.Spec.Scope = scope; - - 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.GetCustomAttributes(true).FirstOrDefault() switch - { - { } attr => attr.Description, - _ => null, - }, - Properties = type.GetProperties() - .Where(p => !IgnoredToplevelProperties.Contains(p.Name.ToLowerInvariant())) - .Where(p => p.GetCustomAttribute() == null) - .Select(p => (Name: TypeMapping.PropertyName(p), Schema: MapProperty(p))) - .ToDictionary(t => t.Name, t => t.Schema), - }); - - version.AdditionalPrinterColumns = MapPrinterColumns(type).ToList() switch - { - { Count: > 0 } l => l, - _ => null, - }; - crd.Spec.Versions = new List { version }; - crd.Validate(); - - return crd; - } - - /// - /// Transpiles the given sequence of Kubernetes entity types to a - /// sequence of objects. - /// The definitions are grouped by version and one stored version is defined. - /// The transpiler fails when multiple stored versions are defined. - /// - /// The sequence of Kubernetes entity types to transpile. - /// A sequence of objects representing the transpiled entity types. - public static IEnumerable Transpile(IEnumerable types) - => types - .Where(type => type.Assembly != typeof(KubernetesEntityAttribute).Assembly) - .Where(type => type.GetCustomAttributes().Any()) - .Where(type => !type.GetCustomAttributes().Any()) - .Select(type => (Props: Transpile(type), - IsStorage: type.GetCustomAttributes().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 IEnumerable MapPrinterColumns(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}.{TypeMapping.PropertyName(prop)}"))); - } - - if (prop.GetCustomAttribute() is not { } attr) - { - continue; - } - - var mapped = MapProperty(prop); - yield return new V1CustomResourceColumnDefinition - { - Name = attr.Name ?? TypeMapping.PropertyName(prop), - JsonPath = $"{path}.{TypeMapping.PropertyName(prop)}", - Type = mapped.Type, - Description = mapped.Description, - Format = mapped.Format, - Priority = attr.Priority switch - { - PrinterColumnPriority.StandardView => 0, - _ => 1, - }, - }; - } - - foreach (var attr in type.GetCustomAttributes(true)) - { - yield return new V1CustomResourceColumnDefinition - { - Name = attr.Name, - JsonPath = attr.JsonPath, - Type = attr.Type, - Description = attr.Description, - Format = attr.Format, - Priority = attr.Priority switch - { - PrinterColumnPriority.StandardView => 0, - _ => 1, - }, - }; - } - } - - private static V1JSONSchemaProps MapProperty(PropertyInfo prop) - { - var props = TypeMapping.Map(prop.PropertyType); - var ctx = prop.ToContextualProperty(); - props.Description ??= prop.GetCustomAttributes(true).FirstOrDefault()?.Description ?? - ctx.GetXmlDocsSummary(); - if (string.IsNullOrWhiteSpace(props.Description)) - { - props.Description = null; - } - - if (ctx.Nullability == Nullability.Nullable) - { - props.Nullable = true; - } - - return AttributeMapping.Map(prop, props); - } - - private static class AttributeMapping - { - private static readonly Func[] Mappers = - { - Mapper((a, p) => - p.ExternalDocs = new V1ExternalDocumentation(a.Description, a.Url)), - Mapper((a, p) => - { - p.MinItems = a.MinItems; - p.MaxItems = a.MaxItems; - }), - Mapper((a, p) => - { - p.MinLength = a.MinLength; - p.MaxLength = a.MaxLength; - }), - Mapper((a, p) => p.MultipleOf = a.Value), - Mapper((a, p) => p.Pattern = a.RegexPattern), Mapper((a, p) => - { - p.Maximum = a.Maximum; - p.ExclusiveMaximum = a.ExclusiveMaximum; - }), - Mapper((a, p) => - { - p.Minimum = a.Minimum; - p.ExclusiveMinimum = a.ExclusiveMinimum; - }), - Mapper((_, p) => p.XKubernetesPreserveUnknownFields = true), - Mapper((_, p) => - { - p.XKubernetesEmbeddedResource = true; - p.XKubernetesPreserveUnknownFields = true; - p.Type = Object; - p.Properties = null; - }), - }; - - public static V1JSONSchemaProps Map(PropertyInfo prop, V1JSONSchemaProps props) => - Mappers.Aggregate(props, (current, mapper) => mapper(prop, current)); - - private static Func Mapper( - Action attributeMapper) - where TAttribute : Attribute - => (prop, props) => - { - if (prop.GetCustomAttribute() is { } attr) - { - attributeMapper(attr, props); - } - - return props; - }; - } - - private static class TypeMapping - { - private static readonly Func, V1JSONSchemaProps?>[] Mappers = - { - Mapper(MapV1ObjectMeta), MapArray, Mapper(MapResourceQuantityDict), MapDictionary, - MapGenericObjectEnumerable, Mapper(MapArbitraryDictionary), MapGenericEnumerable, - Mapper(MapIntOrString), Mapper(MapKubernetesObject), Mapper(MapInt), Mapper(MapLong), Mapper(MapFloat), - Mapper(MapDouble), Mapper(MapString), Mapper(MapBool), Mapper(MapDateTime), Mapper(MapEnum), - Mapper(MapNullableEnum), Mapper(MapComplexType), - }; - - public static V1JSONSchemaProps Map(Type type) - { - foreach (var mapper in Mappers) - { - var mapped = mapper(type, Map); - if (mapped != null) - { - return mapped; - } - } - - throw new ArgumentException($"The given type {type.FullName} is not a valid Kubernetes entity."); - } - - public static string PropertyName(PropertyInfo prop) - { - var name = prop.GetCustomAttribute() switch - { - null => prop.Name, - { Name: { } attrName } => attrName, - }; - -#if NETSTANDARD2_0 - return $"{name.Substring(0, 1).ToLowerInvariant()}{name.Substring(1)}"; -#else - return $"{name[..1].ToLowerInvariant()}{name[1..]}"; -#endif - } - - private static Func, V1JSONSchemaProps?> Mapper( - Func mapper) - => (t, _) => mapper(t); - - private static V1JSONSchemaProps? MapV1ObjectMeta(Type type) - => type == typeof(V1ObjectMeta) - ? new V1JSONSchemaProps { Type = Object } - : null; - - private static V1JSONSchemaProps? MapArray(Type type, Func map) - => type.IsArray && type.GetElementType() != null - ? new V1JSONSchemaProps { Type = Array, Items = map(type.GetElementType()!) } - : null; - - private static V1JSONSchemaProps? MapResourceQuantityDict(Type type) - => !IsSimpleType(type) - && type.IsGenericType - && type.GetGenericTypeDefinition() == typeof(IDictionary<,>) - && type.GenericTypeArguments.Contains(typeof(ResourceQuantity)) - ? new V1JSONSchemaProps { Type = Object, XKubernetesPreserveUnknownFields = true } - : null; - - private static V1JSONSchemaProps? MapDictionary(Type type, Func map) - => !IsSimpleType(type) - && type.IsGenericType - && type.GetGenericTypeDefinition() == typeof(IDictionary<,>) - ? new V1JSONSchemaProps { Type = Object, AdditionalProperties = map(type.GenericTypeArguments[1]) } - : null; - - private static V1JSONSchemaProps? MapGenericObjectEnumerable(Type type, Func map) - => !IsSimpleType(type) && - type.IsGenericType && - type.GetGenericTypeDefinition() == typeof(IEnumerable<>) && - type.GenericTypeArguments.Length == 1 && - type.GenericTypeArguments.Single().IsGenericType && - type.GenericTypeArguments.Single().GetGenericTypeDefinition() == typeof(KeyValuePair<,>) - ? new V1JSONSchemaProps - { - Type = Object, - AdditionalProperties = map(type.GenericTypeArguments.Single().GenericTypeArguments[1]), - } - : null; - - private static V1JSONSchemaProps? MapArbitraryDictionary(Type type) - => !IsSimpleType(type) && - (typeof(IDictionary).IsAssignableFrom(type) || - (type.IsGenericType && - type.GetGenericArguments().FirstOrDefault()?.IsGenericType == true && - type.GetGenericArguments().FirstOrDefault()?.GetGenericTypeDefinition() == - typeof(KeyValuePair<,>))) - ? new V1JSONSchemaProps { Type = Object, XKubernetesPreserveUnknownFields = true } - : null; - - private static V1JSONSchemaProps? MapGenericEnumerable(Type type, Func map) - => !IsSimpleType(type) && IsGenericEnumerableType(type, out Type? closingType) - ? new V1JSONSchemaProps { Type = Array, Items = map(closingType!), } - : null; - - private static V1JSONSchemaProps? MapIntOrString(Type type) - => type == typeof(IntstrIntOrString) - ? new V1JSONSchemaProps { XKubernetesIntOrString = true } - : null; - - private static V1JSONSchemaProps? MapKubernetesObject(Type type) - => typeof(IKubernetesObject).IsAssignableFrom(type) && - type is { IsAbstract: false, IsInterface: false } && - type.Assembly == typeof(IKubernetesObject).Assembly - ? new V1JSONSchemaProps - { - Type = Object, - Properties = null, - XKubernetesPreserveUnknownFields = true, - XKubernetesEmbeddedResource = true, - } - : null; - - private static V1JSONSchemaProps? MapInt(Type type) - => type == typeof(int) || type == typeof(int?) || Nullable.GetUnderlyingType(type) == typeof(int) - ? new V1JSONSchemaProps { Type = Integer, Format = Int32 } - : null; - - private static V1JSONSchemaProps? MapLong(Type type) - => type == typeof(long) || type == typeof(long?) || Nullable.GetUnderlyingType(type) == typeof(long) - ? new V1JSONSchemaProps { Type = Integer, Format = Int64 } - : null; - - private static V1JSONSchemaProps? MapFloat(Type type) - => type == typeof(float) || type == typeof(float?) || Nullable.GetUnderlyingType(type) == typeof(float) - ? new V1JSONSchemaProps { Type = Number, Format = Float } - : null; - - private static V1JSONSchemaProps? MapDouble(Type type) - => type == typeof(double) || type == typeof(double?) || Nullable.GetUnderlyingType(type) == typeof(double) - ? new V1JSONSchemaProps { Type = Number, Format = Double } - : null; - - private static V1JSONSchemaProps? MapString(Type type) - => type == typeof(string) || Nullable.GetUnderlyingType(type) == typeof(string) - ? new V1JSONSchemaProps { Type = String } - : null; - - private static V1JSONSchemaProps? MapBool(Type type) - => type == typeof(bool) || type == typeof(bool?) || Nullable.GetUnderlyingType(type) == typeof(bool) - ? new V1JSONSchemaProps { Type = Boolean } - : null; - - private static V1JSONSchemaProps? MapDateTime(Type type) - => type == typeof(DateTime) || type == typeof(DateTime?) || - Nullable.GetUnderlyingType(type) == typeof(DateTime) - ? new V1JSONSchemaProps { Type = String, Format = DateTime } - : null; - - private static V1JSONSchemaProps? MapEnum(Type type) - => type.IsEnum - ? new V1JSONSchemaProps { Type = String, EnumProperty = Enum.GetNames(type).Cast().ToList() } - : null; - - private static V1JSONSchemaProps? MapNullableEnum(Type type) - => Nullable.GetUnderlyingType(type)?.IsEnum == true - ? new V1JSONSchemaProps - { - Type = String, - EnumProperty = Enum.GetNames(Nullable.GetUnderlyingType(type)!).Cast().ToList(), - } - : null; - - private static V1JSONSchemaProps? MapComplexType(Type type) - => !IsSimpleType(type) - ? new V1JSONSchemaProps - { - Type = Object, - Description = type.GetCustomAttribute()?.Description, - Properties = type - .GetProperties() - .Where(p => p.GetCustomAttribute() == null) - .Select(p => (Name: PropertyName(p), Schema: MapProperty(p))) - .ToDictionary(t => t.Name, t => t.Schema), - Required = type.GetProperties() - .Where(p => p.GetCustomAttribute() != null) - .Where(p => p.GetCustomAttribute() == null) - .Select(PropertyName) - .ToList() switch - { - { Count: > 0 } p => p, - _ => null, - }, - } - : null; - - private static bool IsSimpleType(Type type) => - type.IsPrimitive || - new[] - { - typeof(string), typeof(decimal), typeof(DateTime), typeof(DateTimeOffset), typeof(TimeSpan), - typeof(Guid), - }.Contains(type) || - type.IsEnum || - Convert.GetTypeCode(type) != TypeCode.Object || - (type.IsGenericType && - type.GetGenericTypeDefinition() == typeof(Nullable<>) && - IsSimpleType(type.GetGenericArguments()[0])); - - private static bool IsGenericEnumerableType( - Type type, -#if NET - [NotNullWhen(true)] -#endif - out Type? closingType) - { - if (type.IsGenericType && typeof(IEnumerable<>).IsAssignableFrom(type.GetGenericTypeDefinition())) - { - closingType = type.GetGenericArguments()[0]; - return true; - } - - closingType = type - .GetInterfaces() - .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - .Select(t => t.GetGenericArguments()[0]) - .FirstOrDefault(); - - return closingType != null; - } - } - - private sealed partial class KubernetesVersionComparer : IComparer - { -#if !NET7_0_OR_GREATER - private static readonly Regex KubernetesVersionRegex = - new("^v(?[0-9]+)((?alpha|beta)(?[0-9]+))?$", RegexOptions.Compiled); -#endif - - private enum Stream - { - Alpha = 1, - Beta = 2, - Final = 3, - } - - public int Compare(string? x, string? y) - { - if (x == null || y == null) - { - return StringComparer.CurrentCulture.Compare(x, y); - } - -#if NET7_0_OR_GREATER - var matchX = KubernetesVersionRegex().Match(x); -#else - var matchX = KubernetesVersionRegex.Match(x); -#endif - if (!matchX.Success) - { - return StringComparer.CurrentCulture.Compare(x, y); - } - -#if NET7_0_OR_GREATER - var matchY = KubernetesVersionRegex().Match(y); -#else - var matchY = KubernetesVersionRegex.Match(y); -#endif - if (!matchY.Success) - { - return StringComparer.CurrentCulture.Compare(x, y); - } - - var versionX = ExtractVersion(matchX); - var versionY = ExtractVersion(matchY); - return versionX.CompareTo(versionY); - } - -#if NET7_0_OR_GREATER - [GeneratedRegex("^v(?[0-9]+)((?alpha|beta)(?[0-9]+))?$", RegexOptions.Compiled)] - private static partial Regex KubernetesVersionRegex(); -#endif - - private Version ExtractVersion(Match match) - { - var major = int.Parse(match.Groups["major"].Value); - if (!Enum.TryParse(match.Groups["stream"].Value, true, out var stream)) - { - stream = Stream.Final; - } - - _ = int.TryParse(match.Groups["minor"].Value, out var minor); - return new Version(major, (int)stream, minor); - } - } -} +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +using k8s; +using k8s.Models; + +using KubeOps.Abstractions.Entities; +using KubeOps.Abstractions.Entities.Attributes; + +using Namotion.Reflection; + +namespace KubeOps.Transpiler; + +/// +/// Class for the conversion of C# types to Kubernetes CRDs. +/// +public static partial 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" }; + + /// + /// Transpiles the given Kubernetes entity type to a object. + /// + /// The Kubernetes entity type to transpile. + /// A object representing the transpiled entity type. + /// Thrown if the given type is not a valid Kubernetes entity. + public static V1CustomResourceDefinition Transpile(Type type) + { + var (meta, scope) = Entities.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, + ShortNames = type.GetCustomAttributes(true) + .SelectMany(a => a.ShortNames).ToList() switch + { + { Count: > 0 } p => p, + _ => null, + }, + }; + crd.Spec.Scope = scope; + + 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.GetCustomAttributes(true).FirstOrDefault() switch + { + { } attr => attr.Description, + _ => null, + }, + Properties = type.GetProperties() + .Where(p => !IgnoredToplevelProperties.Contains(p.Name.ToLowerInvariant())) + .Where(p => p.GetCustomAttribute() == null) + .Select(p => (Name: TypeMapping.PropertyName(p), Schema: MapProperty(p))) + .ToDictionary(t => t.Name, t => t.Schema), + }); + + version.AdditionalPrinterColumns = MapPrinterColumns(type).ToList() switch + { + { Count: > 0 } l => l, + _ => null, + }; + crd.Spec.Versions = new List { version }; + crd.Validate(); + + return crd; + } + + /// + /// Transpiles the given sequence of Kubernetes entity types to a + /// sequence of objects. + /// The definitions are grouped by version and one stored version is defined. + /// The transpiler fails when multiple stored versions are defined. + /// + /// The sequence of Kubernetes entity types to transpile. + /// A sequence of objects representing the transpiled entity types. + public static IEnumerable Transpile(IEnumerable types) + => types + .Where(type => type.Assembly != typeof(KubernetesEntityAttribute).Assembly) + .Where(type => type.GetCustomAttributes().Any()) + .Where(type => !type.GetCustomAttributes().Any()) + .Select(type => (Props: Transpile(type), + IsStorage: type.GetCustomAttributes().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 IEnumerable MapPrinterColumns(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}.{TypeMapping.PropertyName(prop)}"))); + } + + if (prop.GetCustomAttribute() is not { } attr) + { + continue; + } + + var mapped = MapProperty(prop); + yield return new V1CustomResourceColumnDefinition + { + Name = attr.Name ?? TypeMapping.PropertyName(prop), + JsonPath = $"{path}.{TypeMapping.PropertyName(prop)}", + Type = mapped.Type, + Description = mapped.Description, + Format = mapped.Format, + Priority = attr.Priority switch + { + PrinterColumnPriority.StandardView => 0, + _ => 1, + }, + }; + } + + foreach (var attr in type.GetCustomAttributes(true)) + { + yield return new V1CustomResourceColumnDefinition + { + Name = attr.Name, + JsonPath = attr.JsonPath, + Type = attr.Type, + Description = attr.Description, + Format = attr.Format, + Priority = attr.Priority switch + { + PrinterColumnPriority.StandardView => 0, + _ => 1, + }, + }; + } + } + + private static V1JSONSchemaProps MapProperty(PropertyInfo prop) + { + var props = TypeMapping.Map(prop.PropertyType); + var ctx = prop.ToContextualProperty(); + props.Description ??= prop.GetCustomAttributes(true).FirstOrDefault()?.Description ?? + ctx.GetXmlDocsSummary(); + if (string.IsNullOrWhiteSpace(props.Description)) + { + props.Description = null; + } + + if (ctx.Nullability == Nullability.Nullable) + { + props.Nullable = true; + } + + return AttributeMapping.Map(prop, props); + } + + private static class AttributeMapping + { + private static readonly Func[] Mappers = + { + Mapper((a, p) => + p.ExternalDocs = new V1ExternalDocumentation(a.Description, a.Url)), + Mapper((a, p) => + { + p.MinItems = a.MinItems; + p.MaxItems = a.MaxItems; + }), + Mapper((a, p) => + { + p.MinLength = a.MinLength; + p.MaxLength = a.MaxLength; + }), + Mapper((a, p) => p.MultipleOf = a.Value), + Mapper((a, p) => p.Pattern = a.RegexPattern), Mapper((a, p) => + { + p.Maximum = a.Maximum; + p.ExclusiveMaximum = a.ExclusiveMaximum; + }), + Mapper((a, p) => + { + p.Minimum = a.Minimum; + p.ExclusiveMinimum = a.ExclusiveMinimum; + }), + Mapper((_, p) => p.XKubernetesPreserveUnknownFields = true), + Mapper((_, p) => + { + p.XKubernetesEmbeddedResource = true; + p.XKubernetesPreserveUnknownFields = true; + p.Type = Object; + p.Properties = null; + }), + }; + + public static V1JSONSchemaProps Map(PropertyInfo prop, V1JSONSchemaProps props) => + Mappers.Aggregate(props, (current, mapper) => mapper(prop, current)); + + private static Func Mapper( + Action attributeMapper) + where TAttribute : Attribute + => (prop, props) => + { + if (prop.GetCustomAttribute() is { } attr) + { + attributeMapper(attr, props); + } + + return props; + }; + } + + private static class TypeMapping + { + private static readonly Func, V1JSONSchemaProps?>[] Mappers = + { + Mapper(MapV1ObjectMeta), MapArray, Mapper(MapResourceQuantityDict), MapDictionary, + MapGenericObjectEnumerable, Mapper(MapArbitraryDictionary), MapGenericEnumerable, + Mapper(MapIntOrString), Mapper(MapKubernetesObject), Mapper(MapInt), Mapper(MapLong), Mapper(MapFloat), + Mapper(MapDouble), Mapper(MapString), Mapper(MapBool), Mapper(MapDateTime), Mapper(MapEnum), + Mapper(MapNullableEnum), Mapper(MapComplexType), + }; + + public static V1JSONSchemaProps Map(Type type) + { + foreach (var mapper in Mappers) + { + var mapped = mapper(type, Map); + if (mapped != null) + { + return mapped; + } + } + + throw new ArgumentException($"The given type {type.FullName} is not a valid Kubernetes entity."); + } + + public static string PropertyName(PropertyInfo prop) + { + var name = prop.GetCustomAttribute() switch + { + null => prop.Name, + { Name: { } attrName } => attrName, + }; + +#if NETSTANDARD2_0 + return $"{name.Substring(0, 1).ToLowerInvariant()}{name.Substring(1)}"; +#else + return $"{name[..1].ToLowerInvariant()}{name[1..]}"; +#endif + } + + private static Func, V1JSONSchemaProps?> Mapper( + Func mapper) + => (t, _) => mapper(t); + + private static V1JSONSchemaProps? MapV1ObjectMeta(Type type) + => type == typeof(V1ObjectMeta) + ? new V1JSONSchemaProps { Type = Object } + : null; + + private static V1JSONSchemaProps? MapArray(Type type, Func map) + => type.IsArray && type.GetElementType() != null + ? new V1JSONSchemaProps { Type = Array, Items = map(type.GetElementType()!) } + : null; + + private static V1JSONSchemaProps? MapResourceQuantityDict(Type type) + => !IsSimpleType(type) + && type.IsGenericType + && type.GetGenericTypeDefinition() == typeof(IDictionary<,>) + && type.GenericTypeArguments.Contains(typeof(ResourceQuantity)) + ? new V1JSONSchemaProps { Type = Object, XKubernetesPreserveUnknownFields = true } + : null; + + private static V1JSONSchemaProps? MapDictionary(Type type, Func map) + => !IsSimpleType(type) + && type.IsGenericType + && type.GetGenericTypeDefinition() == typeof(IDictionary<,>) + ? new V1JSONSchemaProps { Type = Object, AdditionalProperties = map(type.GenericTypeArguments[1]) } + : null; + + private static V1JSONSchemaProps? MapGenericObjectEnumerable(Type type, Func map) + => !IsSimpleType(type) && + type.IsGenericType && + type.GetGenericTypeDefinition() == typeof(IEnumerable<>) && + type.GenericTypeArguments.Length == 1 && + type.GenericTypeArguments.Single().IsGenericType && + type.GenericTypeArguments.Single().GetGenericTypeDefinition() == typeof(KeyValuePair<,>) + ? new V1JSONSchemaProps + { + Type = Object, + AdditionalProperties = map(type.GenericTypeArguments.Single().GenericTypeArguments[1]), + } + : null; + + private static V1JSONSchemaProps? MapArbitraryDictionary(Type type) + => !IsSimpleType(type) && + (typeof(IDictionary).IsAssignableFrom(type) || + (type.IsGenericType && + type.GetGenericArguments().FirstOrDefault()?.IsGenericType == true && + type.GetGenericArguments().FirstOrDefault()?.GetGenericTypeDefinition() == + typeof(KeyValuePair<,>))) + ? new V1JSONSchemaProps { Type = Object, XKubernetesPreserveUnknownFields = true } + : null; + + private static V1JSONSchemaProps? MapGenericEnumerable(Type type, Func map) + => !IsSimpleType(type) && IsGenericEnumerableType(type, out Type? closingType) + ? new V1JSONSchemaProps { Type = Array, Items = map(closingType!), } + : null; + + private static V1JSONSchemaProps? MapIntOrString(Type type) + => type == typeof(IntstrIntOrString) + ? new V1JSONSchemaProps { XKubernetesIntOrString = true } + : null; + + private static V1JSONSchemaProps? MapKubernetesObject(Type type) + => typeof(IKubernetesObject).IsAssignableFrom(type) && + type is { IsAbstract: false, IsInterface: false } && + type.Assembly == typeof(IKubernetesObject).Assembly + ? new V1JSONSchemaProps + { + Type = Object, + Properties = null, + XKubernetesPreserveUnknownFields = true, + XKubernetesEmbeddedResource = true, + } + : null; + + private static V1JSONSchemaProps? MapInt(Type type) + => type == typeof(int) || type == typeof(int?) || Nullable.GetUnderlyingType(type) == typeof(int) + ? new V1JSONSchemaProps { Type = Integer, Format = Int32 } + : null; + + private static V1JSONSchemaProps? MapLong(Type type) + => type == typeof(long) || type == typeof(long?) || Nullable.GetUnderlyingType(type) == typeof(long) + ? new V1JSONSchemaProps { Type = Integer, Format = Int64 } + : null; + + private static V1JSONSchemaProps? MapFloat(Type type) + => type == typeof(float) || type == typeof(float?) || Nullable.GetUnderlyingType(type) == typeof(float) + ? new V1JSONSchemaProps { Type = Number, Format = Float } + : null; + + private static V1JSONSchemaProps? MapDouble(Type type) + => type == typeof(double) || type == typeof(double?) || Nullable.GetUnderlyingType(type) == typeof(double) + ? new V1JSONSchemaProps { Type = Number, Format = Double } + : null; + + private static V1JSONSchemaProps? MapString(Type type) + => type == typeof(string) || Nullable.GetUnderlyingType(type) == typeof(string) + ? new V1JSONSchemaProps { Type = String } + : null; + + private static V1JSONSchemaProps? MapBool(Type type) + => type == typeof(bool) || type == typeof(bool?) || Nullable.GetUnderlyingType(type) == typeof(bool) + ? new V1JSONSchemaProps { Type = Boolean } + : null; + + private static V1JSONSchemaProps? MapDateTime(Type type) + => type == typeof(DateTime) || type == typeof(DateTime?) || + Nullable.GetUnderlyingType(type) == typeof(DateTime) + ? new V1JSONSchemaProps { Type = String, Format = DateTime } + : null; + + private static V1JSONSchemaProps? MapEnum(Type type) + => type.IsEnum + ? new V1JSONSchemaProps { Type = String, EnumProperty = Enum.GetNames(type).Cast().ToList() } + : null; + + private static V1JSONSchemaProps? MapNullableEnum(Type type) + => Nullable.GetUnderlyingType(type)?.IsEnum == true + ? new V1JSONSchemaProps + { + Type = String, + EnumProperty = Enum.GetNames(Nullable.GetUnderlyingType(type)!).Cast().ToList(), + } + : null; + + private static V1JSONSchemaProps? MapComplexType(Type type) + => !IsSimpleType(type) + ? new V1JSONSchemaProps + { + Type = Object, + Description = type.GetCustomAttribute()?.Description, + Properties = type + .GetProperties() + .Where(p => p.GetCustomAttribute() == null) + .Select(p => (Name: PropertyName(p), Schema: MapProperty(p))) + .ToDictionary(t => t.Name, t => t.Schema), + Required = type.GetProperties() + .Where(p => p.GetCustomAttribute() != null) + .Where(p => p.GetCustomAttribute() == null) + .Select(PropertyName) + .ToList() switch + { + { Count: > 0 } p => p, + _ => null, + }, + } + : null; + + private static bool IsSimpleType(Type type) => + type.IsPrimitive || + new[] + { + typeof(string), typeof(decimal), typeof(DateTime), typeof(DateTimeOffset), typeof(TimeSpan), + typeof(Guid), + }.Contains(type) || + type.IsEnum || + Convert.GetTypeCode(type) != TypeCode.Object || + (type.IsGenericType && + type.GetGenericTypeDefinition() == typeof(Nullable<>) && + IsSimpleType(type.GetGenericArguments()[0])); + + private static bool IsGenericEnumerableType( + Type type, +#if NET + [NotNullWhen(true)] +#endif + out Type? closingType) + { + if (type.IsGenericType && typeof(IEnumerable<>).IsAssignableFrom(type.GetGenericTypeDefinition())) + { + closingType = type.GetGenericArguments()[0]; + return true; + } + + closingType = type + .GetInterfaces() + .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + .Select(t => t.GetGenericArguments()[0]) + .FirstOrDefault(); + + return closingType != null; + } + } + + private sealed partial class KubernetesVersionComparer : IComparer + { +#if !NET7_0_OR_GREATER + private static readonly Regex KubernetesVersionRegex = + new("^v(?[0-9]+)((?alpha|beta)(?[0-9]+))?$", RegexOptions.Compiled); +#endif + + private enum Stream + { + Alpha = 1, + Beta = 2, + Final = 3, + } + + public int Compare(string? x, string? y) + { + if (x == null || y == null) + { + return StringComparer.CurrentCulture.Compare(x, y); + } + +#if NET7_0_OR_GREATER + var matchX = KubernetesVersionRegex().Match(x); +#else + var matchX = KubernetesVersionRegex.Match(x); +#endif + if (!matchX.Success) + { + return StringComparer.CurrentCulture.Compare(x, y); + } + +#if NET7_0_OR_GREATER + var matchY = KubernetesVersionRegex().Match(y); +#else + var matchY = KubernetesVersionRegex.Match(y); +#endif + if (!matchY.Success) + { + return StringComparer.CurrentCulture.Compare(x, y); + } + + var versionX = ExtractVersion(matchX); + var versionY = ExtractVersion(matchY); + return versionX.CompareTo(versionY); + } + +#if NET7_0_OR_GREATER + [GeneratedRegex("^v(?[0-9]+)((?alpha|beta)(?[0-9]+))?$", RegexOptions.Compiled)] + private static partial Regex KubernetesVersionRegex(); +#endif + + private Version ExtractVersion(Match match) + { + var major = int.Parse(match.Groups["major"].Value); + if (!Enum.TryParse(match.Groups["stream"].Value, true, out var stream)) + { + stream = Stream.Final; + } + + _ = int.TryParse(match.Groups["minor"].Value, out var minor); + return new Version(major, (int)stream, minor); + } + } +} diff --git a/src/KubeOps.Transpiler/Entities.cs b/src/KubeOps.Transpiler/Entities.cs index 848ffacb..d12557d1 100644 --- a/src/KubeOps.Transpiler/Entities.cs +++ b/src/KubeOps.Transpiler/Entities.cs @@ -1,40 +1,40 @@ -using System.Reflection; - -using k8s.Models; - -using KubeOps.Abstractions.Entities; -using KubeOps.Abstractions.Entities.Attributes; - -namespace KubeOps.Transpiler; - -/// -/// Class for the conversion of types to objects. -/// -public static class Entities -{ - /// - /// Converts the given type to an object. - /// - /// The type to convert. - /// The object representing the type with the scope of the entity. - /// Thrown if the given type is not a valid Kubernetes entity. - public static (EntityMetadata Metadata, string Scope) ToEntityMetadata(Type entityType) - => (entityType.GetCustomAttribute(), - entityType.GetCustomAttribute()) switch - { - (null, _) => throw new ArgumentException("The given type is not a valid Kubernetes entity."), - ({ } attr, var scope) => (new( - Defaulted(attr.Kind, entityType.Name), - Defaulted(attr.ApiVersion, "v1"), - attr.Group, - attr.PluralName), - scope switch - { - null => Enum.GetName(typeof(EntityScope), EntityScope.Namespaced) ?? "namespaced", - _ => Enum.GetName(typeof(EntityScope), scope.Scope) ?? "namespaced", - }), - }; - - private static string Defaulted(string value, string defaultValue) => - string.IsNullOrWhiteSpace(value) ? defaultValue : value; -} +using System.Reflection; + +using k8s.Models; + +using KubeOps.Abstractions.Entities; +using KubeOps.Abstractions.Entities.Attributes; + +namespace KubeOps.Transpiler; + +/// +/// Class for the conversion of types to objects. +/// +public static class Entities +{ + /// + /// Converts the given type to an object. + /// + /// The type to convert. + /// The object representing the type with the scope of the entity. + /// Thrown if the given type is not a valid Kubernetes entity. + public static (EntityMetadata Metadata, string Scope) ToEntityMetadata(Type entityType) + => (entityType.GetCustomAttribute(), + entityType.GetCustomAttribute()) switch + { + (null, _) => throw new ArgumentException("The given type is not a valid Kubernetes entity."), + ({ } attr, var scope) => (new( + Defaulted(attr.Kind, entityType.Name), + Defaulted(attr.ApiVersion, "v1"), + attr.Group, + attr.PluralName), + scope switch + { + null => Enum.GetName(typeof(EntityScope), EntityScope.Namespaced) ?? "namespaced", + _ => Enum.GetName(typeof(EntityScope), scope.Scope) ?? "namespaced", + }), + }; + + private static string Defaulted(string value, string defaultValue) => + string.IsNullOrWhiteSpace(value) ? defaultValue : value; +} diff --git a/src/KubeOps.Transpiler/Rbac.cs b/src/KubeOps.Transpiler/Rbac.cs index d3778bfe..64a7bedd 100644 --- a/src/KubeOps.Transpiler/Rbac.cs +++ b/src/KubeOps.Transpiler/Rbac.cs @@ -1,76 +1,76 @@ -using k8s.Models; - -using KubeOps.Abstractions.Rbac; - -namespace KubeOps.Transpiler; - -public static class Rbac -{ - public static IEnumerable Transpile(IEnumerable attributes) - { - var list = attributes.ToList(); - - var generic = list - .OfType() - .Select(a => new V1PolicyRule - { - ApiGroups = a.Groups, - Resources = a.Resources, - NonResourceURLs = a.Urls, - Verbs = ConvertToStrings(a.Verbs), - }); - - var entities = list - .OfType() - .SelectMany(attribute => - attribute.Entities.Select(type => (EntityType: type, attribute.Verbs))) - .GroupBy(e => e.EntityType) - .Select( - group => ( - Crd: Entities.ToEntityMetadata(group.Key), - Verbs: group.Aggregate(RbacVerb.None, (accumulator, element) => accumulator | element.Verbs))) - .GroupBy(group => group.Verbs) - .Select( - group => ( - Verbs: group.Key, - Crds: group.Select(element => element.Crd).ToList())) - .Select( - group => new V1PolicyRule - { - ApiGroups = group.Crds.Select(crd => crd.Metadata.Group).Distinct().ToList(), - Resources = group.Crds.Select(crd => crd.Metadata.PluralName).Distinct().ToList(), - Verbs = ConvertToStrings(group.Verbs), - }); - - var entityStatus = list - .OfType() - .SelectMany(attribute => attribute.Entities.Select(type => (EntityType: type, attribute.Verbs))) - .Where(e => e.EntityType.GetProperty("Status") != null) - .GroupBy(e => e.EntityType) - .Select(group => Entities.ToEntityMetadata(group.Key)) - .Select( - crd => new V1PolicyRule() - { - ApiGroups = new[] { crd.Metadata.Group }, - Resources = new[] { $"{crd.Metadata.PluralName}/status" }, - Verbs = ConvertToStrings(RbacVerb.Get | RbacVerb.Patch | RbacVerb.Update), - }); - - return generic.Concat(entities).Concat(entityStatus); - } - - private static string[] ConvertToStrings(RbacVerb verbs) => verbs switch - { - RbacVerb.None => Array.Empty(), - _ when verbs.HasFlag(RbacVerb.All) => new[] { "*" }, - _ => -#if NETSTANDARD - Enum.GetValues(typeof(RbacVerb)).Cast() -#else - Enum.GetValues() -#endif - .Where(v => verbs.HasFlag(v) && v != RbacVerb.All && v != RbacVerb.None) - .Select(v => v.ToString().ToLowerInvariant()) - .ToArray(), - }; -} +using k8s.Models; + +using KubeOps.Abstractions.Rbac; + +namespace KubeOps.Transpiler; + +public static class Rbac +{ + public static IEnumerable Transpile(IEnumerable attributes) + { + var list = attributes.ToList(); + + var generic = list + .OfType() + .Select(a => new V1PolicyRule + { + ApiGroups = a.Groups, + Resources = a.Resources, + NonResourceURLs = a.Urls, + Verbs = ConvertToStrings(a.Verbs), + }); + + var entities = list + .OfType() + .SelectMany(attribute => + attribute.Entities.Select(type => (EntityType: type, attribute.Verbs))) + .GroupBy(e => e.EntityType) + .Select( + group => ( + Crd: Entities.ToEntityMetadata(group.Key), + Verbs: group.Aggregate(RbacVerb.None, (accumulator, element) => accumulator | element.Verbs))) + .GroupBy(group => group.Verbs) + .Select( + group => ( + Verbs: group.Key, + Crds: group.Select(element => element.Crd).ToList())) + .Select( + group => new V1PolicyRule + { + ApiGroups = group.Crds.Select(crd => crd.Metadata.Group).Distinct().ToList(), + Resources = group.Crds.Select(crd => crd.Metadata.PluralName).Distinct().ToList(), + Verbs = ConvertToStrings(group.Verbs), + }); + + var entityStatus = list + .OfType() + .SelectMany(attribute => attribute.Entities.Select(type => (EntityType: type, attribute.Verbs))) + .Where(e => e.EntityType.GetProperty("Status") != null) + .GroupBy(e => e.EntityType) + .Select(group => Entities.ToEntityMetadata(group.Key)) + .Select( + crd => new V1PolicyRule() + { + ApiGroups = new[] { crd.Metadata.Group }, + Resources = new[] { $"{crd.Metadata.PluralName}/status" }, + Verbs = ConvertToStrings(RbacVerb.Get | RbacVerb.Patch | RbacVerb.Update), + }); + + return generic.Concat(entities).Concat(entityStatus); + } + + private static string[] ConvertToStrings(RbacVerb verbs) => verbs switch + { + RbacVerb.None => Array.Empty(), + _ when verbs.HasFlag(RbacVerb.All) => new[] { "*" }, + _ => +#if NETSTANDARD + Enum.GetValues(typeof(RbacVerb)).Cast() +#else + Enum.GetValues() +#endif + .Where(v => verbs.HasFlag(v) && v != RbacVerb.All && v != RbacVerb.None) + .Select(v => v.ToString().ToLowerInvariant()) + .ToArray(), + }; +} diff --git a/test/KubeOps.Cli.Test/Generator/CertificateGenerator.Test.cs b/test/KubeOps.Cli.Test/Generator/CertificateGenerator.Test.cs index 17b2de0d..5e06609f 100644 --- a/test/KubeOps.Cli.Test/Generator/CertificateGenerator.Test.cs +++ b/test/KubeOps.Cli.Test/Generator/CertificateGenerator.Test.cs @@ -1,97 +1,97 @@ -using System.CommandLine; -using System.CommandLine.Invocation; - -using FluentAssertions; - -using KubeOps.Cli.Commands.Generator; - -using Org.BouncyCastle.Crypto; -using Org.BouncyCastle.OpenSsl; -using Org.BouncyCastle.X509; - -using Spectre.Console.Testing; - -namespace KubeOps.Cli.Test.Generator; - -public class CertificateGeneratorTest -{ - [Fact] - public async Task Should_Execute() - { - var console = new TestConsole(); - - var cmd = CertificateGenerator.Command; - var ctx = new InvocationContext( - cmd.Parse("server", "namespace")); - - await CertificateGenerator.Handler(console, ctx); - - ctx.ExitCode.Should().Be(ExitCodes.Success); - } - - [Theory] - [InlineData("ca.pem")] - [InlineData("ca-key.pem")] - [InlineData("svc.pem")] - [InlineData("svc-key.pem")] - public async Task Should_Generate_Certificate_Files(string file) - { - var console = new TestConsole(); - - var cmd = CertificateGenerator.Command; - var ctx = new InvocationContext( - cmd.Parse("server", "namespace")); - - await CertificateGenerator.Handler(console, ctx); - - console.Output.Should().Contain($"File: {file}"); - } - - [Fact] - public async Task Should_Generate_Valid_Certificates() - { - var console = new TestConsole(); - - var cmd = CertificateGenerator.Command; - var ctx = new InvocationContext( - cmd.Parse("server", "namespace")); - - await CertificateGenerator.Handler(console, ctx); - - var output = console.Lines.ToArray(); - var caCertString = string.Join('\n', output[4..15]); - var caCertKeyString = string.Join('\n', output[18..23]); - var srvCertString = string.Join('\n', output[26..42]); - var srvCertKeyString = string.Join('\n', output[45..50]); - - if (new PemReader(new StringReader(caCertString)).ReadObject() is not X509Certificate caCert) - { - Assert.Fail("Could not parse CA certificate."); - return; - } - - if (new PemReader(new StringReader(caCertKeyString)).ReadObject() is not AsymmetricCipherKeyPair caKey) - { - Assert.Fail("Could not parse CA private key."); - return; - } - - if (new PemReader(new StringReader(srvCertString)).ReadObject() is not X509Certificate srvCert) - { - Assert.Fail("Could not parse server certificate."); - return; - } - - if (new PemReader(new StringReader(srvCertKeyString)).ReadObject() is not AsymmetricCipherKeyPair) - { - Assert.Fail("Could not parse server private key."); - return; - } - - caCert.IsValidNow.Should().BeTrue(); - caCert.Verify(caKey.Public); - - srvCert.IsValidNow.Should().BeTrue(); - srvCert.Verify(caKey.Public); - } -} +using System.CommandLine; +using System.CommandLine.Invocation; + +using FluentAssertions; + +using KubeOps.Cli.Commands.Generator; + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.X509; + +using Spectre.Console.Testing; + +namespace KubeOps.Cli.Test.Generator; + +public class CertificateGeneratorTest +{ + [Fact] + public async Task Should_Execute() + { + var console = new TestConsole(); + + var cmd = CertificateGenerator.Command; + var ctx = new InvocationContext( + cmd.Parse("server", "namespace")); + + await CertificateGenerator.Handler(console, ctx); + + ctx.ExitCode.Should().Be(ExitCodes.Success); + } + + [Theory] + [InlineData("ca.pem")] + [InlineData("ca-key.pem")] + [InlineData("svc.pem")] + [InlineData("svc-key.pem")] + public async Task Should_Generate_Certificate_Files(string file) + { + var console = new TestConsole(); + + var cmd = CertificateGenerator.Command; + var ctx = new InvocationContext( + cmd.Parse("server", "namespace")); + + await CertificateGenerator.Handler(console, ctx); + + console.Output.Should().Contain($"File: {file}"); + } + + [Fact] + public async Task Should_Generate_Valid_Certificates() + { + var console = new TestConsole(); + + var cmd = CertificateGenerator.Command; + var ctx = new InvocationContext( + cmd.Parse("server", "namespace")); + + await CertificateGenerator.Handler(console, ctx); + + var output = console.Lines.ToArray(); + var caCertString = string.Join('\n', output[4..15]); + var caCertKeyString = string.Join('\n', output[18..23]); + var srvCertString = string.Join('\n', output[26..42]); + var srvCertKeyString = string.Join('\n', output[45..50]); + + if (new PemReader(new StringReader(caCertString)).ReadObject() is not X509Certificate caCert) + { + Assert.Fail("Could not parse CA certificate."); + return; + } + + if (new PemReader(new StringReader(caCertKeyString)).ReadObject() is not AsymmetricCipherKeyPair caKey) + { + Assert.Fail("Could not parse CA private key."); + return; + } + + if (new PemReader(new StringReader(srvCertString)).ReadObject() is not X509Certificate srvCert) + { + Assert.Fail("Could not parse server certificate."); + return; + } + + if (new PemReader(new StringReader(srvCertKeyString)).ReadObject() is not AsymmetricCipherKeyPair) + { + Assert.Fail("Could not parse server private key."); + return; + } + + caCert.IsValidNow.Should().BeTrue(); + caCert.Verify(caKey.Public); + + srvCert.IsValidNow.Should().BeTrue(); + srvCert.Verify(caKey.Public); + } +} diff --git a/test/KubeOps.Cli.Test/Management/Install.Integration.Test.cs b/test/KubeOps.Cli.Test/Management/Install.Integration.Test.cs index 16fb030e..fb47a763 100644 --- a/test/KubeOps.Cli.Test/Management/Install.Integration.Test.cs +++ b/test/KubeOps.Cli.Test/Management/Install.Integration.Test.cs @@ -1,30 +1,30 @@ -using System.CommandLine; -using System.CommandLine.Invocation; - -using k8s; - -using KubeOps.Cli.Commands.Management; - -using Spectre.Console.Testing; - -namespace KubeOps.Cli.Test.Management; - -public class InstallIntegrationTest -{ - private static readonly string ProjectPath = - Path.Join(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "examples", "Operator", - "Operator.csproj"); - - [Fact(Skip = - "For some reason, the MetadataReferences are not loaded when the assembly parser is used from a test project.")] - public async Task Should_Install_Crds_In_Cluster() - { - var console = new TestConsole(); - var client = new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()); - var cmd = Install.Command; - var ctx = new InvocationContext( - cmd.Parse(ProjectPath, "-f")); - - await Install.Handler(console, client, ctx); - } -} +using System.CommandLine; +using System.CommandLine.Invocation; + +using k8s; + +using KubeOps.Cli.Commands.Management; + +using Spectre.Console.Testing; + +namespace KubeOps.Cli.Test.Management; + +public class InstallIntegrationTest +{ + private static readonly string ProjectPath = + Path.Join(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "examples", "Operator", + "Operator.csproj"); + + [Fact(Skip = + "For some reason, the MetadataReferences are not loaded when the assembly parser is used from a test project.")] + public async Task Should_Install_Crds_In_Cluster() + { + var console = new TestConsole(); + var client = new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()); + var cmd = Install.Command; + var ctx = new InvocationContext( + cmd.Parse(ProjectPath, "-f")); + + await Install.Handler(console, client, ctx); + } +} diff --git a/test/KubeOps.Generator.Test/ControllerRegistrationGenerator.Test.cs b/test/KubeOps.Generator.Test/ControllerRegistrationGenerator.Test.cs new file mode 100644 index 00000000..96e885cd --- /dev/null +++ b/test/KubeOps.Generator.Test/ControllerRegistrationGenerator.Test.cs @@ -0,0 +1,57 @@ +using FluentAssertions; + +using KubeOps.Generator.Generators; + +using Microsoft.CodeAnalysis.CSharp; + +namespace KubeOps.Generator.Test; + +public class ControllerRegistrationGeneratorTest +{ + [Theory] + [InlineData("", """ + using KubeOps.Abstractions.Builder; + + public static class ControllerRegistrations + { + public static IOperatorBuilder RegisterControllers(this IOperatorBuilder builder) + { + return builder; + } + } + """)] + [InlineData(""" + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + public class V1TestEntity : IKubernetesObject + { + } + + public class V1TestEntityController : IEntityController + { + } + """, """ + using KubeOps.Abstractions.Builder; + + public static class ControllerRegistrations + { + public static IOperatorBuilder RegisterControllers(this IOperatorBuilder builder) + { + builder.AddController(); + return builder; + } + } + """)] + public void Should_Generate_Correct_Code(string input, string expectedResult) + { + var inputCompilation = input.CreateCompilation(); + expectedResult = expectedResult.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new ControllerRegistrationGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); + + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("ControllerRegistrations.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } +} diff --git a/test/KubeOps.Generator.Test/EntityDefinitionGenerator.Test.cs b/test/KubeOps.Generator.Test/EntityDefinitionGenerator.Test.cs new file mode 100644 index 00000000..1918f4c0 --- /dev/null +++ b/test/KubeOps.Generator.Test/EntityDefinitionGenerator.Test.cs @@ -0,0 +1,82 @@ +using FluentAssertions; + +using KubeOps.Generator.Generators; + +using Microsoft.CodeAnalysis.CSharp; + +namespace KubeOps.Generator.Test; + +public class EntityDefinitionGeneratorTest +{ + [Theory] + [InlineData("", """ + using KubeOps.Abstractions.Builder; + using KubeOps.Abstractions.Entities; + + public static class EntityDefinitions + { + public static IOperatorBuilder RegisterEntities(this IOperatorBuilder builder) + { + return builder; + } + } + """)] + [InlineData(""" + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + public class V1TestEntity : IKubernetesObject + { + } + """, """ + using KubeOps.Abstractions.Builder; + using KubeOps.Abstractions.Entities; + + public static class EntityDefinitions + { + public static readonly EntityMetadata V1TestEntity = new("TestEntity", "v1", "testing.dev", null); + public static IOperatorBuilder RegisterEntities(this IOperatorBuilder builder) + { + builder.AddEntity(V1TestEntity); + return builder; + } + } + """)] + [InlineData(""" + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + public class V1TestEntity : IKubernetesObject + { + } + + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "AnotherEntity")] + public class V1AnotherEntity : IKubernetesObject + { + } + """, """ + using KubeOps.Abstractions.Builder; + using KubeOps.Abstractions.Entities; + + public static class EntityDefinitions + { + public static readonly EntityMetadata V1TestEntity = new("TestEntity", "v1", "testing.dev", null); + public static readonly EntityMetadata V1AnotherEntity = new("AnotherEntity", "v1", "testing.dev", null); + public static IOperatorBuilder RegisterEntities(this IOperatorBuilder builder) + { + builder.AddEntity(V1TestEntity); + builder.AddEntity(V1AnotherEntity); + return builder; + } + } + """)] + public void Should_Generate_Correct_Code(string input, string expectedResult) + { + var inputCompilation = input.CreateCompilation(); + expectedResult = expectedResult.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new EntityDefinitionGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); + + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("EntityDefinitions.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } +} diff --git a/test/KubeOps.Generator.Test/KubeOps.Generator.Test.csproj b/test/KubeOps.Generator.Test/KubeOps.Generator.Test.csproj index 61718a1f..49fca81d 100644 --- a/test/KubeOps.Generator.Test/KubeOps.Generator.Test.csproj +++ b/test/KubeOps.Generator.Test/KubeOps.Generator.Test.csproj @@ -1,3 +1,12 @@ - + + + + + + + + + + diff --git a/test/KubeOps.Generator.Test/OperatorBuilderGenerator.Test.cs b/test/KubeOps.Generator.Test/OperatorBuilderGenerator.Test.cs new file mode 100644 index 00000000..2afee846 --- /dev/null +++ b/test/KubeOps.Generator.Test/OperatorBuilderGenerator.Test.cs @@ -0,0 +1,38 @@ +using FluentAssertions; + +using KubeOps.Generator.Generators; + +using Microsoft.CodeAnalysis.CSharp; + +namespace KubeOps.Generator.Test; + +public class OperatorBuilderGeneratorTest +{ + [Fact] + public void Should_Generate_Correct_Code() + { + var inputCompilation = string.Empty.CreateCompilation(); + var expectedResult = + """ + using KubeOps.Abstractions.Builder; + + public static class OperatorBuilderExtensions + { + public static IOperatorBuilder RegisterResources(this IOperatorBuilder builder) + { + builder.RegisterEntities(); + builder.RegisterControllers(); + return builder; + } + } + """.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new OperatorBuilderGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); + + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("OperatorBuilder.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } +} diff --git a/test/KubeOps.Generator.Test/TestHelperExtensions.cs b/test/KubeOps.Generator.Test/TestHelperExtensions.cs new file mode 100644 index 00000000..47795bd8 --- /dev/null +++ b/test/KubeOps.Generator.Test/TestHelperExtensions.cs @@ -0,0 +1,20 @@ +using System.Reflection; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace KubeOps.Generator.Test; + +internal static class TestHelperExtensions +{ + public static Compilation CreateCompilation(this string source) + { + var compilation = CSharpCompilation.Create( + "compilation", + new[] { CSharpSyntaxTree.ParseText(source) }, + new[] { MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) }, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + return compilation; + } +} diff --git a/test/KubeOps.KubernetesClient.Test/GlobalUsings.cs b/test/KubeOps.KubernetesClient.Test/GlobalUsings.cs index c802f448..e1065597 100644 --- a/test/KubeOps.KubernetesClient.Test/GlobalUsings.cs +++ b/test/KubeOps.KubernetesClient.Test/GlobalUsings.cs @@ -1 +1 @@ -global using Xunit; +global using Xunit; diff --git a/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs b/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs index 013db68b..190b22eb 100644 --- a/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs +++ b/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs @@ -1,138 +1,138 @@ -using FluentAssertions; - -using k8s.Models; - -namespace KubeOps.KubernetesClient.Test; - -public class KubernetesClientTest : IDisposable -{ - private readonly IKubernetesClient _client = - new KubernetesClient(new("ConfigMap", "v1", null, "configmaps")); - - private readonly IList _objects = new List(); - - [Fact] - public async Task Should_Return_Namespace() - { - var ns = await _client.GetCurrentNamespace(); - ns.Should().Be("default"); - } - - [Fact] - public async Task Should_Create_Some_Object() - { - var config = await _client.Create( - new V1ConfigMap - { - Kind = V1ConfigMap.KubeKind, - ApiVersion = V1ConfigMap.KubeApiVersion, - Metadata = new(name: RandomName(), namespaceProperty: "default"), - Data = new Dictionary { { "Hello", "World" } }, - }); - - _objects.Add(config); - - config.Metadata.Should().NotBeNull(); - config.Metadata.ResourceVersion.Should().NotBeNullOrWhiteSpace(); - } - - [Fact] - public async Task Should_Update_Some_Object() - { - var config = await _client.Create( - new V1ConfigMap - { - Kind = V1ConfigMap.KubeKind, - ApiVersion = V1ConfigMap.KubeApiVersion, - Metadata = new V1ObjectMeta(name: RandomName(), namespaceProperty: "default"), - Data = new Dictionary { { "Hello", "World" } }, - }); - var r1 = config.Metadata.ResourceVersion; - _objects.Add(config); - - config.Data.Add("test", "value"); - config = await _client.Update(config); - var r2 = config.Metadata.ResourceVersion; - - r1.Should().NotBe(r2); - } - - [Fact] - public async Task Should_List_Some_Objects() - { - var config1 = await _client.Create( - new V1ConfigMap - { - Kind = V1ConfigMap.KubeKind, - ApiVersion = V1ConfigMap.KubeApiVersion, - Metadata = new(name: RandomName(), namespaceProperty: "default"), - Data = new Dictionary { { "Hello", "World" } }, - }); - var config2 = await _client.Create( - new V1ConfigMap - { - Kind = V1ConfigMap.KubeKind, - ApiVersion = V1ConfigMap.KubeApiVersion, - Metadata = new(name: RandomName(), namespaceProperty: "default"), - Data = new Dictionary { { "Hello", "World" } }, - }); - - _objects.Add(config1); - _objects.Add(config2); - - var configs = await _client.List("default"); - - // there are _at least_ 2 config maps (the two that were created) - configs.Count.Should().BeGreaterOrEqualTo(2); - } - - [Fact] - public async Task Should_Delete_Some_Object() - { - var config1 = await _client.Create( - new V1ConfigMap - { - Kind = V1ConfigMap.KubeKind, - ApiVersion = V1ConfigMap.KubeApiVersion, - Metadata = new(name: RandomName(), namespaceProperty: "default"), - Data = new Dictionary { { "Hello", "World" } }, - }); - var config2 = await _client.Create( - new V1ConfigMap - { - Kind = V1ConfigMap.KubeKind, - ApiVersion = V1ConfigMap.KubeApiVersion, - Metadata = new(name: RandomName(), namespaceProperty: "default"), - Data = new Dictionary { { "Hello", "World" } }, - }); - _objects.Add(config1); - - var configs = await _client.List("default"); - configs.Count.Should().BeGreaterOrEqualTo(2); - - await _client.Delete(config2); - - configs = await _client.List("default"); - configs.Count.Should().BeGreaterOrEqualTo(1); - } - - [Fact] - public async Task Should_Not_Throw_On_Not_Found_Delete() - { - var config = new V1ConfigMap - { - Kind = V1ConfigMap.KubeKind, - ApiVersion = V1ConfigMap.KubeApiVersion, - Metadata = new(name: RandomName(), namespaceProperty: "default"), - Data = new Dictionary { { "Hello", "World" } }, - }; - await _client.Delete(config); - } - - public void Dispose() - { - _client.Delete(_objects).Wait(); - } - - private static string RandomName() => "cm-" + Guid.NewGuid().ToString().ToLower(); -} +using FluentAssertions; + +using k8s.Models; + +namespace KubeOps.KubernetesClient.Test; + +public class KubernetesClientTest : IDisposable +{ + private readonly IKubernetesClient _client = + new KubernetesClient(new("ConfigMap", "v1", null, "configmaps")); + + private readonly IList _objects = new List(); + + [Fact] + public async Task Should_Return_Namespace() + { + var ns = await _client.GetCurrentNamespace(); + ns.Should().Be("default"); + } + + [Fact] + public async Task Should_Create_Some_Object() + { + var config = await _client.Create( + new V1ConfigMap + { + Kind = V1ConfigMap.KubeKind, + ApiVersion = V1ConfigMap.KubeApiVersion, + Metadata = new(name: RandomName(), namespaceProperty: "default"), + Data = new Dictionary { { "Hello", "World" } }, + }); + + _objects.Add(config); + + config.Metadata.Should().NotBeNull(); + config.Metadata.ResourceVersion.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Should_Update_Some_Object() + { + var config = await _client.Create( + new V1ConfigMap + { + Kind = V1ConfigMap.KubeKind, + ApiVersion = V1ConfigMap.KubeApiVersion, + Metadata = new V1ObjectMeta(name: RandomName(), namespaceProperty: "default"), + Data = new Dictionary { { "Hello", "World" } }, + }); + var r1 = config.Metadata.ResourceVersion; + _objects.Add(config); + + config.Data.Add("test", "value"); + config = await _client.Update(config); + var r2 = config.Metadata.ResourceVersion; + + r1.Should().NotBe(r2); + } + + [Fact] + public async Task Should_List_Some_Objects() + { + var config1 = await _client.Create( + new V1ConfigMap + { + Kind = V1ConfigMap.KubeKind, + ApiVersion = V1ConfigMap.KubeApiVersion, + Metadata = new(name: RandomName(), namespaceProperty: "default"), + Data = new Dictionary { { "Hello", "World" } }, + }); + var config2 = await _client.Create( + new V1ConfigMap + { + Kind = V1ConfigMap.KubeKind, + ApiVersion = V1ConfigMap.KubeApiVersion, + Metadata = new(name: RandomName(), namespaceProperty: "default"), + Data = new Dictionary { { "Hello", "World" } }, + }); + + _objects.Add(config1); + _objects.Add(config2); + + var configs = await _client.List("default"); + + // there are _at least_ 2 config maps (the two that were created) + configs.Count.Should().BeGreaterOrEqualTo(2); + } + + [Fact] + public async Task Should_Delete_Some_Object() + { + var config1 = await _client.Create( + new V1ConfigMap + { + Kind = V1ConfigMap.KubeKind, + ApiVersion = V1ConfigMap.KubeApiVersion, + Metadata = new(name: RandomName(), namespaceProperty: "default"), + Data = new Dictionary { { "Hello", "World" } }, + }); + var config2 = await _client.Create( + new V1ConfigMap + { + Kind = V1ConfigMap.KubeKind, + ApiVersion = V1ConfigMap.KubeApiVersion, + Metadata = new(name: RandomName(), namespaceProperty: "default"), + Data = new Dictionary { { "Hello", "World" } }, + }); + _objects.Add(config1); + + var configs = await _client.List("default"); + configs.Count.Should().BeGreaterOrEqualTo(2); + + await _client.Delete(config2); + + configs = await _client.List("default"); + configs.Count.Should().BeGreaterOrEqualTo(1); + } + + [Fact] + public async Task Should_Not_Throw_On_Not_Found_Delete() + { + var config = new V1ConfigMap + { + Kind = V1ConfigMap.KubeKind, + ApiVersion = V1ConfigMap.KubeApiVersion, + Metadata = new(name: RandomName(), namespaceProperty: "default"), + Data = new Dictionary { { "Hello", "World" } }, + }; + await _client.Delete(config); + } + + public void Dispose() + { + _client.Delete(_objects).Wait(); + } + + private static string RandomName() => "cm-" + Guid.NewGuid().ToString().ToLower(); +} diff --git a/test/KubeOps.Transpiler.Test/Crds.Test.cs b/test/KubeOps.Transpiler.Test/Crds.Test.cs index d4b3e420..dec9bbea 100644 --- a/test/KubeOps.Transpiler.Test/Crds.Test.cs +++ b/test/KubeOps.Transpiler.Test/Crds.Test.cs @@ -1,547 +1,547 @@ -using System.Reflection; -using System.Text.Json.Serialization; - -using FluentAssertions; - -using k8s.Models; - -using KubeOps.Transpiler.Test.TestEntities; - -namespace KubeOps.Transpiler.Test; - -internal static class Str -{ - public static string ToCamelCase(this string t) => $"{t[..1].ToLowerInvariant()}{t[1..]}"; -} - -public class CrdsTest -{ - private readonly Type _testSpecEntity = typeof(TestSpecEntity); - private readonly Type _testClusterSpecEntity = typeof(TestClusterSpecEntity); - private readonly Type _testStatusEntity = typeof(TestStatusEntity); - - [Fact] - public void Should_Ignore_Entity() - { - var crds = Crds.Transpile(new[] { typeof(IgnoredEntity) }); - crds.Count().Should().Be(0); - } - - [Fact] - public void Should_Ignore_NonEntity() - { - var crds = Crds.Transpile(new[] { typeof(NonEntity) }); - crds.Count().Should().Be(0); - } - - [Fact] - public void Should_Ignore_Kubernetes_Entities() - { - var crds = Crds.Transpile(new[] { typeof(V1Pod) }); - crds.Count().Should().Be(0); - } - - [Fact] - public void Should_Set_Highest_Version_As_Storage() - { - var crds = Crds.Transpile(new[] - { - typeof(V1Alpha1VersionedEntity), typeof(V1Beta1VersionedEntity), typeof(V2Beta2VersionedEntity), - typeof(V2VersionedEntity), typeof(V1VersionedEntity), typeof(V1AttributeVersionedEntity), - typeof(V2AttributeVersionedEntity), - }); - var crd = crds.First(c => c.Spec.Names.Kind == "VersionedEntity"); - crd.Spec.Versions.Count(v => v.Storage).Should().Be(1); - crd.Spec.Versions.First(v => v.Storage).Name.Should().Be("v2"); - } - - [Fact] - public void Should_Set_Storage_When_Attribute_Is_Set() - { - var crds = Crds.Transpile(new[] - { - typeof(V1Alpha1VersionedEntity), typeof(V1Beta1VersionedEntity), typeof(V2Beta2VersionedEntity), - typeof(V2VersionedEntity), typeof(V1VersionedEntity), typeof(V1AttributeVersionedEntity), - typeof(V2AttributeVersionedEntity), - }); - var crd = crds.First(c => c.Spec.Names.Kind == "AttributeVersionedEntity"); - crd.Spec.Versions.Count(v => v.Storage).Should().Be(1); - crd.Spec.Versions.First(v => v.Storage).Name.Should().Be("v1"); - } - - [Fact] - public void Should_Add_Multiple_Versions_To_Crd() - { - var crds = Crds.Transpile(new[] - { - typeof(V1Alpha1VersionedEntity), typeof(V1Beta1VersionedEntity), typeof(V2Beta2VersionedEntity), - typeof(V2VersionedEntity), typeof(V1VersionedEntity), typeof(V1AttributeVersionedEntity), - typeof(V2AttributeVersionedEntity), - }).ToList(); - crds - .First(c => c.Spec.Names.Kind == "VersionedEntity") - .Spec.Versions.Should() - .HaveCount(5); - crds - .First(c => c.Spec.Names.Kind == "AttributeVersionedEntity") - .Spec.Versions.Should() - .HaveCount(2); - } - - [Fact] - public void Should_Add_ShortNames_To_Crd() - { - var crd = Crds.Transpile(typeof(TestStatusEntity)); - crd.Spec.Names.ShortNames.Should() - .NotBeNull() - .And - .Contain(new[] { "foo", "bar", "baz" }); - } - - [Fact] - public void Should_Use_Correct_CRD() - { - var crd = Crds.Transpile(_testSpecEntity); - var (ced, scope) = Entities.ToEntityMetadata(_testSpecEntity); - - crd.Kind.Should().Be(V1CustomResourceDefinition.KubeKind); - crd.Metadata.Name.Should().Be($"{ced.PluralName}.{ced.Group}"); - crd.Spec.Names.Kind.Should().Be(ced.Kind); - crd.Spec.Names.ListKind.Should().Be(ced.ListKind); - crd.Spec.Names.Singular.Should().Be(ced.SingularName); - crd.Spec.Names.Plural.Should().Be(ced.PluralName); - crd.Spec.Scope.Should().Be(scope); - } - - [Fact] - public void Should_Add_Status_SubResource_If_Present() - { - var crd = Crds.Transpile(_testStatusEntity); - crd.Spec.Versions.First().Subresources.Status.Should().NotBeNull(); - } - - [Fact] - public void Should_Not_Add_Status_SubResource_If_Absent() - { - var crd = Crds.Transpile(_testSpecEntity); - crd.Spec.Versions.First().Subresources?.Status?.Should().BeNull(); - } - - [Theory] - [InlineData("Int", "integer", "int32")] - [InlineData("Long", "integer", "int64")] - [InlineData("Float", "number", "float")] - [InlineData("Double", "number", "double")] - [InlineData("String", "string", null)] - [InlineData("Bool", "boolean", null)] - [InlineData("DateTime", "string", "date-time")] - [InlineData("Enum", "string", null)] - [InlineData(nameof(TestSpecEntitySpec.GenericDictionary), "object", null)] - [InlineData(nameof(TestSpecEntitySpec.KeyValueEnumerable), "object", null)] - public void Should_Set_The_Correct_Type_And_Format_For_Types(string fieldName, string typeName, string? format) - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Type.Should().Be("object"); - - var normalField = specProperties.Properties[$"normal{fieldName}"]; - normalField.Type.Should().Be(typeName); - normalField.Format.Should().Be(format); - normalField.Nullable.Should().BeNull(); - - var nullableField = specProperties.Properties[$"nullable{fieldName}"]; - nullableField.Type.Should().Be(typeName); - nullableField.Format.Should().Be(format); - nullableField.Nullable.Should().BeTrue(); - } - - [Theory] - [InlineData(nameof(TestSpecEntitySpec.StringArray), "string", null)] - [InlineData(nameof(TestSpecEntitySpec.NullableStringArray), "string", true)] - [InlineData(nameof(TestSpecEntitySpec.EnumerableInteger), "integer", null)] - [InlineData(nameof(TestSpecEntitySpec.EnumerableNullableInteger), "integer", null)] - [InlineData(nameof(TestSpecEntitySpec.IntegerList), "integer", null)] - [InlineData(nameof(TestSpecEntitySpec.IntegerHashSet), "integer", null)] - [InlineData(nameof(TestSpecEntitySpec.IntegerISet), "integer", null)] - [InlineData(nameof(TestSpecEntitySpec.IntegerIReadOnlySet), "integer", null)] - public void Should_Set_The_Correct_Array_Type(string property, string expectedType, bool? expectedNullable) - { - var propertyName = property.ToCamelCase(); - var crd = Crds.Transpile(_testSpecEntity); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - - var normalField = specProperties.Properties[propertyName]; - normalField.Type.Should().Be("array"); - (normalField.Items as V1JSONSchemaProps)?.Type?.Should().Be(expectedType); - normalField.Nullable.Should().Be(expectedNullable); - } - - [Theory] - [InlineData(nameof(TestSpecEntitySpec.ComplexItemsEnumerable))] - [InlineData(nameof(TestSpecEntitySpec.ComplexItemsList))] - [InlineData(nameof(TestSpecEntitySpec.ComplexItemsIList))] - [InlineData(nameof(TestSpecEntitySpec.ComplexItemsReadOnlyList))] - [InlineData(nameof(TestSpecEntitySpec.ComplexItemsCollection))] - [InlineData(nameof(TestSpecEntitySpec.ComplexItemsICollection))] - [InlineData(nameof(TestSpecEntitySpec.ComplexItemsReadOnlyCollection))] - [InlineData(nameof(TestSpecEntitySpec.ComplexItemsDerivedList))] - public void Should_Set_The_Correct_Complex_Array_Type(string property) - { - var propertyName = property.ToCamelCase(); - var crd = Crds.Transpile(_testSpecEntity); - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - - var complexItemsArray = specProperties.Properties[propertyName]; - complexItemsArray.Type.Should().Be("array"); - (complexItemsArray.Items as V1JSONSchemaProps)?.Type?.Should().Be("object"); - complexItemsArray.Nullable.Should().BeNull(); - var subProps = (complexItemsArray.Items as V1JSONSchemaProps)!.Properties; - - var subName = subProps["name"]; - subName?.Type.Should().Be("string"); - } - - [Fact] - public void Should_Set_Description_On_Class() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Description.Should().NotBe(""); - } - - [Fact] - public void Should_Set_Description() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var field = specProperties.Properties["description"]; - - field.Description.Should().NotBe(""); - } - - [Fact] - public void Should_Set_ExternalDocs() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var field = specProperties.Properties["externalDocs"]; - - field.ExternalDocs.Url.Should().NotBe(""); - } - - [Fact] - public void Should_Set_ExternalDocs_Description() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var field = specProperties.Properties["externalDocsWithDescription"]; - - field.ExternalDocs.Description.Should().NotBe(""); - } - - [Fact] - public void Should_Set_Item_Information() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var field = specProperties.Properties["items"]; - - field.Type.Should().Be("array"); - (field.Items as V1JSONSchemaProps)?.Type?.Should().Be("string"); - field.MaxItems.Should().Be(42); - field.MinItems.Should().Be(13); - } - - [Fact] - public void Should_Set_Length_Information() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var field = specProperties.Properties["length"]; - - field.MinLength.Should().Be(2); - field.MaxLength.Should().Be(42); - } - - [Fact] - public void Should_Set_MultipleOf() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var field = specProperties.Properties["multipleOf"]; - - field.MultipleOf.Should().Be(15); - } - - [Fact] - public void Should_Set_Pattern() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var field = specProperties.Properties["pattern"]; - - field.Pattern.Should().Be(@"/\d*/"); - } - - [Fact] - public void Should_Set_RangeMinimum() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var field = specProperties.Properties["rangeMinimum"]; - - field.Minimum.Should().Be(15); - field.ExclusiveMinimum.Should().BeTrue(); - } - - [Fact] - public void Should_Set_RangeMaximum() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var field = specProperties.Properties["rangeMaximum"]; - - field.Maximum.Should().Be(15); - field.ExclusiveMaximum.Should().BeTrue(); - } - - [Fact] - public void Should_Set_Required() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Required.Should().Contain("required"); - } - - [Fact] - public void Should_Set_Required_Null_If_No_Required() - { - var crd = Crds.Transpile(_testStatusEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Required.Should().BeNull(); - } - - [Fact] - public void Should_Set_Preserve_Unknown_Fields() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Properties["preserveUnknownFields"].XKubernetesPreserveUnknownFields.Should().BeTrue(); - } - - [Fact] - public void Should_Set_Preserve_Unknown_Fields_On_Dictionaries() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Properties["dictionary"].XKubernetesPreserveUnknownFields.Should().BeTrue(); - } - - [Fact] - public void Should_Not_Set_Preserve_Unknown_Fields_On_Generic_Dictionaries() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Properties["genericDictionary"].XKubernetesPreserveUnknownFields.Should().BeNull(); - } - - [Fact] - public void Should_Not_Set_Preserve_Unknown_Fields_On_KeyValuePair_Enumerable() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Properties["keyValueEnumerable"].XKubernetesPreserveUnknownFields.Should().BeNull(); - } - - [Fact] - public void Should_Not_Set_Properties_On_Dictionaries() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Properties["dictionary"].Properties.Should().BeNull(); - } - - [Fact] - public void Should_Not_Set_Properties_On_Generic_Dictionaries() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Properties["genericDictionary"].Properties.Should().BeNull(); - } - - [Fact] - public void Should_Not_Set_Properties_On_KeyValuePair_Enumerable() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Properties["keyValueEnumerable"].Properties.Should().BeNull(); - } - - [Fact] - public void Should_Set_AdditionalProperties_On_Dictionaries_For_Value_type() - { - const string propertyName = nameof(TestSpecEntity.Spec.GenericDictionary); - var valueType = _testSpecEntity - .GetProperty(nameof(TestSpecEntity.Spec))! - .PropertyType.GetProperty(propertyName)! - .PropertyType.GetGenericArguments()[1] - .Name; - - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var valueItems = - specProperties.Properties[propertyName.ToCamelCase()].AdditionalProperties as V1JSONSchemaProps; - valueItems.Should().NotBeNull(); - valueItems!.Type.Should().Be(valueType.ToCamelCase()); - } - - [Fact] - public void Should_Set_AdditionalProperties_On_KeyValuePair_For_Value_type() - { - const string propertyName = nameof(TestSpecEntity.Spec.KeyValueEnumerable); - var valueType = _testSpecEntity - .GetProperty(nameof(TestSpecEntity.Spec))! - .PropertyType.GetProperty(propertyName)! - .PropertyType.GetGenericArguments()[0] - .GetGenericArguments()[1] - .Name; - - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var valueItems = - specProperties.Properties[propertyName.ToCamelCase()].AdditionalProperties as V1JSONSchemaProps; - valueItems.Should().NotBeNull(); - valueItems!.Type.Should().Be(valueType.ToCamelCase()); - } - - [Fact] - public void Should_Set_IntOrString() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Properties["intOrString"].Properties.Should().BeNull(); - specProperties.Properties["intOrString"].XKubernetesIntOrString.Should().BeTrue(); - } - - [Theory] - [InlineData(nameof(TestSpecEntitySpec.KubernetesObject))] - [InlineData(nameof(TestSpecEntitySpec.Pod))] - public void Should_Map_Embedded_Resources(string property) - { - var crd = Crds.Transpile(_testSpecEntity); - var propertyName = property.ToCamelCase(); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Properties[propertyName].Type.Should().Be("object"); - specProperties.Properties[propertyName].Properties.Should().BeNull(); - specProperties.Properties[propertyName].XKubernetesPreserveUnknownFields.Should().BeTrue(); - specProperties.Properties[propertyName].XKubernetesEmbeddedResource.Should().BeTrue(); - } - - [Fact] - public void Should_Map_List_Of_Embedded_Resource() - { - var crd = Crds.Transpile(_testSpecEntity); - var propertyName = nameof(TestSpecEntitySpec.Pods).ToCamelCase(); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var arrayProperty = specProperties.Properties[propertyName]; - arrayProperty.Type.Should().Be("array"); - - var items = arrayProperty.Items as V1JSONSchemaProps; - items?.Type?.Should().Be("object"); - items?.XKubernetesPreserveUnknownFields.Should().BeTrue(); - items?.XKubernetesEmbeddedResource?.Should().BeTrue(); - } - - [Fact] - public void Should_Use_PropertyName_From_JsonPropertyAttribute() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - const string propertyNameFromType = nameof(TestSpecEntitySpec.PropertyWithJsonAttribute); - var propertyNameFromAttribute = typeof(TestSpecEntitySpec) - .GetProperty(propertyNameFromType) - ?.GetCustomAttribute() - ?.Name; - specProperties.Properties.Should().ContainKey(propertyNameFromAttribute?.ToCamelCase()); - specProperties.Properties.Should().NotContainKey(propertyNameFromType.ToCamelCase()); - } - - [Fact] - public void Should_Add_AdditionalPrinterColumns() - { - var crd = Crds.Transpile(_testSpecEntity); - var apc = crd.Spec.Versions.First().AdditionalPrinterColumns; - - apc.Should().NotBeNull(); - apc.Should() - .ContainSingle( - def => def.JsonPath == ".spec.normalString" && def.Name == "normalString" && def.Priority == 0); - apc.Should().ContainSingle(def => def.JsonPath == ".spec.normalInt" && def.Priority == 1); - apc.Should().ContainSingle(def => def.JsonPath == ".spec.normalLong" && def.Name == "OtherName"); - } - - [Fact] - public void Must_Not_Contain_Ignored_TopLevel_Properties() - { - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties; - specProperties.Should().NotContainKeys("metadata", "apiVersion", "kind"); - } - - [Fact] - public void Should_Add_GenericAdditionalPrinterColumns() - { - var crd = Crds.Transpile(_testSpecEntity); - var apc = crd.Spec.Versions.First().AdditionalPrinterColumns; - - apc.Should().NotBeNull(); - apc.Should().ContainSingle(def => def.JsonPath == ".metadata.creationTimestamp" && def.Name == "Age"); - } - - [Fact] - public void Should_Correctly_Use_Entity_Scope_Attribute() - { - var scopedCrd = Crds.Transpile(_testSpecEntity); - var clusterCrd = Crds.Transpile(_testClusterSpecEntity); - - scopedCrd.Spec.Scope.Should().Be("Namespaced"); - clusterCrd.Spec.Scope.Should().Be("Cluster"); - } - - [Fact] - public void Should_Not_Contain_Ignored_Property() - { - const string propertyName = nameof(TestSpecEntity.Spec.IgnoredProperty); - var crd = Crds.Transpile(_testSpecEntity); - - var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - specProperties.Properties.Should().NotContainKey(propertyName.ToCamelCase()); - } -} +using System.Reflection; +using System.Text.Json.Serialization; + +using FluentAssertions; + +using k8s.Models; + +using KubeOps.Transpiler.Test.TestEntities; + +namespace KubeOps.Transpiler.Test; + +internal static class Str +{ + public static string ToCamelCase(this string t) => $"{t[..1].ToLowerInvariant()}{t[1..]}"; +} + +public class CrdsTest +{ + private readonly Type _testSpecEntity = typeof(TestSpecEntity); + private readonly Type _testClusterSpecEntity = typeof(TestClusterSpecEntity); + private readonly Type _testStatusEntity = typeof(TestStatusEntity); + + [Fact] + public void Should_Ignore_Entity() + { + var crds = Crds.Transpile(new[] { typeof(IgnoredEntity) }); + crds.Count().Should().Be(0); + } + + [Fact] + public void Should_Ignore_NonEntity() + { + var crds = Crds.Transpile(new[] { typeof(NonEntity) }); + crds.Count().Should().Be(0); + } + + [Fact] + public void Should_Ignore_Kubernetes_Entities() + { + var crds = Crds.Transpile(new[] { typeof(V1Pod) }); + crds.Count().Should().Be(0); + } + + [Fact] + public void Should_Set_Highest_Version_As_Storage() + { + var crds = Crds.Transpile(new[] + { + typeof(V1Alpha1VersionedEntity), typeof(V1Beta1VersionedEntity), typeof(V2Beta2VersionedEntity), + typeof(V2VersionedEntity), typeof(V1VersionedEntity), typeof(V1AttributeVersionedEntity), + typeof(V2AttributeVersionedEntity), + }); + var crd = crds.First(c => c.Spec.Names.Kind == "VersionedEntity"); + crd.Spec.Versions.Count(v => v.Storage).Should().Be(1); + crd.Spec.Versions.First(v => v.Storage).Name.Should().Be("v2"); + } + + [Fact] + public void Should_Set_Storage_When_Attribute_Is_Set() + { + var crds = Crds.Transpile(new[] + { + typeof(V1Alpha1VersionedEntity), typeof(V1Beta1VersionedEntity), typeof(V2Beta2VersionedEntity), + typeof(V2VersionedEntity), typeof(V1VersionedEntity), typeof(V1AttributeVersionedEntity), + typeof(V2AttributeVersionedEntity), + }); + var crd = crds.First(c => c.Spec.Names.Kind == "AttributeVersionedEntity"); + crd.Spec.Versions.Count(v => v.Storage).Should().Be(1); + crd.Spec.Versions.First(v => v.Storage).Name.Should().Be("v1"); + } + + [Fact] + public void Should_Add_Multiple_Versions_To_Crd() + { + var crds = Crds.Transpile(new[] + { + typeof(V1Alpha1VersionedEntity), typeof(V1Beta1VersionedEntity), typeof(V2Beta2VersionedEntity), + typeof(V2VersionedEntity), typeof(V1VersionedEntity), typeof(V1AttributeVersionedEntity), + typeof(V2AttributeVersionedEntity), + }).ToList(); + crds + .First(c => c.Spec.Names.Kind == "VersionedEntity") + .Spec.Versions.Should() + .HaveCount(5); + crds + .First(c => c.Spec.Names.Kind == "AttributeVersionedEntity") + .Spec.Versions.Should() + .HaveCount(2); + } + + [Fact] + public void Should_Add_ShortNames_To_Crd() + { + var crd = Crds.Transpile(typeof(TestStatusEntity)); + crd.Spec.Names.ShortNames.Should() + .NotBeNull() + .And + .Contain(new[] { "foo", "bar", "baz" }); + } + + [Fact] + public void Should_Use_Correct_CRD() + { + var crd = Crds.Transpile(_testSpecEntity); + var (ced, scope) = Entities.ToEntityMetadata(_testSpecEntity); + + crd.Kind.Should().Be(V1CustomResourceDefinition.KubeKind); + crd.Metadata.Name.Should().Be($"{ced.PluralName}.{ced.Group}"); + crd.Spec.Names.Kind.Should().Be(ced.Kind); + crd.Spec.Names.ListKind.Should().Be(ced.ListKind); + crd.Spec.Names.Singular.Should().Be(ced.SingularName); + crd.Spec.Names.Plural.Should().Be(ced.PluralName); + crd.Spec.Scope.Should().Be(scope); + } + + [Fact] + public void Should_Add_Status_SubResource_If_Present() + { + var crd = Crds.Transpile(_testStatusEntity); + crd.Spec.Versions.First().Subresources.Status.Should().NotBeNull(); + } + + [Fact] + public void Should_Not_Add_Status_SubResource_If_Absent() + { + var crd = Crds.Transpile(_testSpecEntity); + crd.Spec.Versions.First().Subresources?.Status?.Should().BeNull(); + } + + [Theory] + [InlineData("Int", "integer", "int32")] + [InlineData("Long", "integer", "int64")] + [InlineData("Float", "number", "float")] + [InlineData("Double", "number", "double")] + [InlineData("String", "string", null)] + [InlineData("Bool", "boolean", null)] + [InlineData("DateTime", "string", "date-time")] + [InlineData("Enum", "string", null)] + [InlineData(nameof(TestSpecEntitySpec.GenericDictionary), "object", null)] + [InlineData(nameof(TestSpecEntitySpec.KeyValueEnumerable), "object", null)] + public void Should_Set_The_Correct_Type_And_Format_For_Types(string fieldName, string typeName, string? format) + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Type.Should().Be("object"); + + var normalField = specProperties.Properties[$"normal{fieldName}"]; + normalField.Type.Should().Be(typeName); + normalField.Format.Should().Be(format); + normalField.Nullable.Should().BeNull(); + + var nullableField = specProperties.Properties[$"nullable{fieldName}"]; + nullableField.Type.Should().Be(typeName); + nullableField.Format.Should().Be(format); + nullableField.Nullable.Should().BeTrue(); + } + + [Theory] + [InlineData(nameof(TestSpecEntitySpec.StringArray), "string", null)] + [InlineData(nameof(TestSpecEntitySpec.NullableStringArray), "string", true)] + [InlineData(nameof(TestSpecEntitySpec.EnumerableInteger), "integer", null)] + [InlineData(nameof(TestSpecEntitySpec.EnumerableNullableInteger), "integer", null)] + [InlineData(nameof(TestSpecEntitySpec.IntegerList), "integer", null)] + [InlineData(nameof(TestSpecEntitySpec.IntegerHashSet), "integer", null)] + [InlineData(nameof(TestSpecEntitySpec.IntegerISet), "integer", null)] + [InlineData(nameof(TestSpecEntitySpec.IntegerIReadOnlySet), "integer", null)] + public void Should_Set_The_Correct_Array_Type(string property, string expectedType, bool? expectedNullable) + { + var propertyName = property.ToCamelCase(); + var crd = Crds.Transpile(_testSpecEntity); + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + + var normalField = specProperties.Properties[propertyName]; + normalField.Type.Should().Be("array"); + (normalField.Items as V1JSONSchemaProps)?.Type?.Should().Be(expectedType); + normalField.Nullable.Should().Be(expectedNullable); + } + + [Theory] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsEnumerable))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsList))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsIList))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsReadOnlyList))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsCollection))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsICollection))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsReadOnlyCollection))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsDerivedList))] + public void Should_Set_The_Correct_Complex_Array_Type(string property) + { + var propertyName = property.ToCamelCase(); + var crd = Crds.Transpile(_testSpecEntity); + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + + var complexItemsArray = specProperties.Properties[propertyName]; + complexItemsArray.Type.Should().Be("array"); + (complexItemsArray.Items as V1JSONSchemaProps)?.Type?.Should().Be("object"); + complexItemsArray.Nullable.Should().BeNull(); + var subProps = (complexItemsArray.Items as V1JSONSchemaProps)!.Properties; + + var subName = subProps["name"]; + subName?.Type.Should().Be("string"); + } + + [Fact] + public void Should_Set_Description_On_Class() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Description.Should().NotBe(""); + } + + [Fact] + public void Should_Set_Description() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var field = specProperties.Properties["description"]; + + field.Description.Should().NotBe(""); + } + + [Fact] + public void Should_Set_ExternalDocs() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var field = specProperties.Properties["externalDocs"]; + + field.ExternalDocs.Url.Should().NotBe(""); + } + + [Fact] + public void Should_Set_ExternalDocs_Description() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var field = specProperties.Properties["externalDocsWithDescription"]; + + field.ExternalDocs.Description.Should().NotBe(""); + } + + [Fact] + public void Should_Set_Item_Information() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var field = specProperties.Properties["items"]; + + field.Type.Should().Be("array"); + (field.Items as V1JSONSchemaProps)?.Type?.Should().Be("string"); + field.MaxItems.Should().Be(42); + field.MinItems.Should().Be(13); + } + + [Fact] + public void Should_Set_Length_Information() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var field = specProperties.Properties["length"]; + + field.MinLength.Should().Be(2); + field.MaxLength.Should().Be(42); + } + + [Fact] + public void Should_Set_MultipleOf() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var field = specProperties.Properties["multipleOf"]; + + field.MultipleOf.Should().Be(15); + } + + [Fact] + public void Should_Set_Pattern() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var field = specProperties.Properties["pattern"]; + + field.Pattern.Should().Be(@"/\d*/"); + } + + [Fact] + public void Should_Set_RangeMinimum() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var field = specProperties.Properties["rangeMinimum"]; + + field.Minimum.Should().Be(15); + field.ExclusiveMinimum.Should().BeTrue(); + } + + [Fact] + public void Should_Set_RangeMaximum() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var field = specProperties.Properties["rangeMaximum"]; + + field.Maximum.Should().Be(15); + field.ExclusiveMaximum.Should().BeTrue(); + } + + [Fact] + public void Should_Set_Required() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Required.Should().Contain("required"); + } + + [Fact] + public void Should_Set_Required_Null_If_No_Required() + { + var crd = Crds.Transpile(_testStatusEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Required.Should().BeNull(); + } + + [Fact] + public void Should_Set_Preserve_Unknown_Fields() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Properties["preserveUnknownFields"].XKubernetesPreserveUnknownFields.Should().BeTrue(); + } + + [Fact] + public void Should_Set_Preserve_Unknown_Fields_On_Dictionaries() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Properties["dictionary"].XKubernetesPreserveUnknownFields.Should().BeTrue(); + } + + [Fact] + public void Should_Not_Set_Preserve_Unknown_Fields_On_Generic_Dictionaries() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Properties["genericDictionary"].XKubernetesPreserveUnknownFields.Should().BeNull(); + } + + [Fact] + public void Should_Not_Set_Preserve_Unknown_Fields_On_KeyValuePair_Enumerable() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Properties["keyValueEnumerable"].XKubernetesPreserveUnknownFields.Should().BeNull(); + } + + [Fact] + public void Should_Not_Set_Properties_On_Dictionaries() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Properties["dictionary"].Properties.Should().BeNull(); + } + + [Fact] + public void Should_Not_Set_Properties_On_Generic_Dictionaries() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Properties["genericDictionary"].Properties.Should().BeNull(); + } + + [Fact] + public void Should_Not_Set_Properties_On_KeyValuePair_Enumerable() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Properties["keyValueEnumerable"].Properties.Should().BeNull(); + } + + [Fact] + public void Should_Set_AdditionalProperties_On_Dictionaries_For_Value_type() + { + const string propertyName = nameof(TestSpecEntity.Spec.GenericDictionary); + var valueType = _testSpecEntity + .GetProperty(nameof(TestSpecEntity.Spec))! + .PropertyType.GetProperty(propertyName)! + .PropertyType.GetGenericArguments()[1] + .Name; + + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var valueItems = + specProperties.Properties[propertyName.ToCamelCase()].AdditionalProperties as V1JSONSchemaProps; + valueItems.Should().NotBeNull(); + valueItems!.Type.Should().Be(valueType.ToCamelCase()); + } + + [Fact] + public void Should_Set_AdditionalProperties_On_KeyValuePair_For_Value_type() + { + const string propertyName = nameof(TestSpecEntity.Spec.KeyValueEnumerable); + var valueType = _testSpecEntity + .GetProperty(nameof(TestSpecEntity.Spec))! + .PropertyType.GetProperty(propertyName)! + .PropertyType.GetGenericArguments()[0] + .GetGenericArguments()[1] + .Name; + + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var valueItems = + specProperties.Properties[propertyName.ToCamelCase()].AdditionalProperties as V1JSONSchemaProps; + valueItems.Should().NotBeNull(); + valueItems!.Type.Should().Be(valueType.ToCamelCase()); + } + + [Fact] + public void Should_Set_IntOrString() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Properties["intOrString"].Properties.Should().BeNull(); + specProperties.Properties["intOrString"].XKubernetesIntOrString.Should().BeTrue(); + } + + [Theory] + [InlineData(nameof(TestSpecEntitySpec.KubernetesObject))] + [InlineData(nameof(TestSpecEntitySpec.Pod))] + public void Should_Map_Embedded_Resources(string property) + { + var crd = Crds.Transpile(_testSpecEntity); + var propertyName = property.ToCamelCase(); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Properties[propertyName].Type.Should().Be("object"); + specProperties.Properties[propertyName].Properties.Should().BeNull(); + specProperties.Properties[propertyName].XKubernetesPreserveUnknownFields.Should().BeTrue(); + specProperties.Properties[propertyName].XKubernetesEmbeddedResource.Should().BeTrue(); + } + + [Fact] + public void Should_Map_List_Of_Embedded_Resource() + { + var crd = Crds.Transpile(_testSpecEntity); + var propertyName = nameof(TestSpecEntitySpec.Pods).ToCamelCase(); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + var arrayProperty = specProperties.Properties[propertyName]; + arrayProperty.Type.Should().Be("array"); + + var items = arrayProperty.Items as V1JSONSchemaProps; + items?.Type?.Should().Be("object"); + items?.XKubernetesPreserveUnknownFields.Should().BeTrue(); + items?.XKubernetesEmbeddedResource?.Should().BeTrue(); + } + + [Fact] + public void Should_Use_PropertyName_From_JsonPropertyAttribute() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + const string propertyNameFromType = nameof(TestSpecEntitySpec.PropertyWithJsonAttribute); + var propertyNameFromAttribute = typeof(TestSpecEntitySpec) + .GetProperty(propertyNameFromType) + ?.GetCustomAttribute() + ?.Name; + specProperties.Properties.Should().ContainKey(propertyNameFromAttribute?.ToCamelCase()); + specProperties.Properties.Should().NotContainKey(propertyNameFromType.ToCamelCase()); + } + + [Fact] + public void Should_Add_AdditionalPrinterColumns() + { + var crd = Crds.Transpile(_testSpecEntity); + var apc = crd.Spec.Versions.First().AdditionalPrinterColumns; + + apc.Should().NotBeNull(); + apc.Should() + .ContainSingle( + def => def.JsonPath == ".spec.normalString" && def.Name == "normalString" && def.Priority == 0); + apc.Should().ContainSingle(def => def.JsonPath == ".spec.normalInt" && def.Priority == 1); + apc.Should().ContainSingle(def => def.JsonPath == ".spec.normalLong" && def.Name == "OtherName"); + } + + [Fact] + public void Must_Not_Contain_Ignored_TopLevel_Properties() + { + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties; + specProperties.Should().NotContainKeys("metadata", "apiVersion", "kind"); + } + + [Fact] + public void Should_Add_GenericAdditionalPrinterColumns() + { + var crd = Crds.Transpile(_testSpecEntity); + var apc = crd.Spec.Versions.First().AdditionalPrinterColumns; + + apc.Should().NotBeNull(); + apc.Should().ContainSingle(def => def.JsonPath == ".metadata.creationTimestamp" && def.Name == "Age"); + } + + [Fact] + public void Should_Correctly_Use_Entity_Scope_Attribute() + { + var scopedCrd = Crds.Transpile(_testSpecEntity); + var clusterCrd = Crds.Transpile(_testClusterSpecEntity); + + scopedCrd.Spec.Scope.Should().Be("Namespaced"); + clusterCrd.Spec.Scope.Should().Be("Cluster"); + } + + [Fact] + public void Should_Not_Contain_Ignored_Property() + { + const string propertyName = nameof(TestSpecEntity.Spec.IgnoredProperty); + var crd = Crds.Transpile(_testSpecEntity); + + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; + specProperties.Properties.Should().NotContainKey(propertyName.ToCamelCase()); + } +} diff --git a/test/KubeOps.Transpiler.Test/Rbac.Test.cs b/test/KubeOps.Transpiler.Test/Rbac.Test.cs index 2f855f14..5289a30d 100644 --- a/test/KubeOps.Transpiler.Test/Rbac.Test.cs +++ b/test/KubeOps.Transpiler.Test/Rbac.Test.cs @@ -1,58 +1,58 @@ -using System.Reflection; - -using FluentAssertions; - -using KubeOps.Abstractions.Rbac; -using KubeOps.Transpiler.Test.TestEntities; - -namespace KubeOps.Transpiler.Test; - -public class RbacTest -{ - [Fact] - public void Should_Calculate_Max_Verbs_For_Types() - { - var role = Rbac.Transpile(typeof(RbacTest1).GetCustomAttributes()).First(); - role.Resources.Should().Contain("rbactest1s"); - role.Verbs.Should().Contain(new[] { "get", "update", "delete" }); - } - - [Fact] - public void Should_Correctly_Calculate_All_Verb() - { - var role = Rbac.Transpile(typeof(RbacTest2).GetCustomAttributes()).First(); - role.Resources.Should().Contain("rbactest2s"); - role.Verbs.Should().Contain("*").And.HaveCount(1); - } - - [Fact] - public void Should_Group_Same_Types_Together() - { - var roles = Rbac.Transpile(typeof(RbacTest3).GetCustomAttributes()).ToList(); - roles.Should() - .Contain( - rule => rule.Resources.Contains("rbactest1s")); - roles.Should() - .Contain( - rule => rule.Resources.Contains("rbactest2s")); - roles.Should().HaveCount(2); - } - - [Fact] - public void Should_Group_Types_With_Same_Verbs_Together() - { - var roles = Rbac.Transpile(typeof(RbacTest4).GetCustomAttributes()).ToList(); - roles.Should() - .Contain( - rule => rule.Resources.Contains("rbactest1s") && - rule.Resources.Contains("rbactest4s") && - rule.Verbs.Contains("get") && - rule.Verbs.Contains("update")); - roles.Should() - .Contain( - rule => rule.Resources.Contains("rbactest2s") && - rule.Resources.Contains("rbactest3s") && - rule.Verbs.Contains("delete")); - roles.Should().HaveCount(2); - } -} +using System.Reflection; + +using FluentAssertions; + +using KubeOps.Abstractions.Rbac; +using KubeOps.Transpiler.Test.TestEntities; + +namespace KubeOps.Transpiler.Test; + +public class RbacTest +{ + [Fact] + public void Should_Calculate_Max_Verbs_For_Types() + { + var role = Rbac.Transpile(typeof(RbacTest1).GetCustomAttributes()).First(); + role.Resources.Should().Contain("rbactest1s"); + role.Verbs.Should().Contain(new[] { "get", "update", "delete" }); + } + + [Fact] + public void Should_Correctly_Calculate_All_Verb() + { + var role = Rbac.Transpile(typeof(RbacTest2).GetCustomAttributes()).First(); + role.Resources.Should().Contain("rbactest2s"); + role.Verbs.Should().Contain("*").And.HaveCount(1); + } + + [Fact] + public void Should_Group_Same_Types_Together() + { + var roles = Rbac.Transpile(typeof(RbacTest3).GetCustomAttributes()).ToList(); + roles.Should() + .Contain( + rule => rule.Resources.Contains("rbactest1s")); + roles.Should() + .Contain( + rule => rule.Resources.Contains("rbactest2s")); + roles.Should().HaveCount(2); + } + + [Fact] + public void Should_Group_Types_With_Same_Verbs_Together() + { + var roles = Rbac.Transpile(typeof(RbacTest4).GetCustomAttributes()).ToList(); + roles.Should() + .Contain( + rule => rule.Resources.Contains("rbactest1s") && + rule.Resources.Contains("rbactest4s") && + rule.Verbs.Contains("get") && + rule.Verbs.Contains("update")); + roles.Should() + .Contain( + rule => rule.Resources.Contains("rbactest2s") && + rule.Resources.Contains("rbactest3s") && + rule.Verbs.Contains("delete")); + roles.Should().HaveCount(2); + } +} diff --git a/test/KubeOps.Transpiler.Test/TestEntities/Base.cs b/test/KubeOps.Transpiler.Test/TestEntities/Base.cs index 4904a898..93e24c16 100644 --- a/test/KubeOps.Transpiler.Test/TestEntities/Base.cs +++ b/test/KubeOps.Transpiler.Test/TestEntities/Base.cs @@ -1,9 +1,9 @@ -using k8s; - -namespace KubeOps.Transpiler.Test.TestEntities; - -public abstract class Base : IKubernetesObject -{ - public string ApiVersion { get; set; } = string.Empty; - public string Kind { get; set; } = string.Empty; -} +using k8s; + +namespace KubeOps.Transpiler.Test.TestEntities; + +public abstract class Base : IKubernetesObject +{ + public string ApiVersion { get; set; } = string.Empty; + public string Kind { get; set; } = string.Empty; +} diff --git a/test/KubeOps.Transpiler.Test/TestEntities/RbacEntities.cs b/test/KubeOps.Transpiler.Test/TestEntities/RbacEntities.cs index 058f30c2..bf93dc67 100644 --- a/test/KubeOps.Transpiler.Test/TestEntities/RbacEntities.cs +++ b/test/KubeOps.Transpiler.Test/TestEntities/RbacEntities.cs @@ -1,39 +1,39 @@ -using k8s.Models; - -using KubeOps.Abstractions.Rbac; - -namespace KubeOps.Transpiler.Test.TestEntities; - -[KubernetesEntity(Group = "test", ApiVersion = "v1")] -[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Get)] -[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Update)] -[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Delete)] -public class RbacTest1 : Base -{ -} - -[KubernetesEntity(Group = "test", ApiVersion = "v1")] -[EntityRbac(typeof(RbacTest2), Verbs = RbacVerb.All)] -[EntityRbac(typeof(RbacTest2), Verbs = RbacVerb.Delete)] -public class RbacTest2 : Base -{ -} - -[KubernetesEntity(Group = "test", ApiVersion = "v1")] -[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Get)] -[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Update)] -[EntityRbac(typeof(RbacTest2), Verbs = RbacVerb.Delete)] -public class RbacTest3 : Base -{ -} - -[KubernetesEntity(Group = "test", ApiVersion = "v1")] -[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Get)] -[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Update)] -[EntityRbac(typeof(RbacTest2), Verbs = RbacVerb.Delete)] -[EntityRbac(typeof(RbacTest2), Verbs = RbacVerb.Delete)] -[EntityRbac(typeof(RbacTest3), Verbs = RbacVerb.Delete)] -[EntityRbac(typeof(RbacTest4), Verbs = RbacVerb.Get | RbacVerb.Update)] -public class RbacTest4 : Base -{ -} +using k8s.Models; + +using KubeOps.Abstractions.Rbac; + +namespace KubeOps.Transpiler.Test.TestEntities; + +[KubernetesEntity(Group = "test", ApiVersion = "v1")] +[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Get)] +[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Update)] +[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Delete)] +public class RbacTest1 : Base +{ +} + +[KubernetesEntity(Group = "test", ApiVersion = "v1")] +[EntityRbac(typeof(RbacTest2), Verbs = RbacVerb.All)] +[EntityRbac(typeof(RbacTest2), Verbs = RbacVerb.Delete)] +public class RbacTest2 : Base +{ +} + +[KubernetesEntity(Group = "test", ApiVersion = "v1")] +[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Get)] +[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Update)] +[EntityRbac(typeof(RbacTest2), Verbs = RbacVerb.Delete)] +public class RbacTest3 : Base +{ +} + +[KubernetesEntity(Group = "test", ApiVersion = "v1")] +[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Get)] +[EntityRbac(typeof(RbacTest1), Verbs = RbacVerb.Update)] +[EntityRbac(typeof(RbacTest2), Verbs = RbacVerb.Delete)] +[EntityRbac(typeof(RbacTest2), Verbs = RbacVerb.Delete)] +[EntityRbac(typeof(RbacTest3), Verbs = RbacVerb.Delete)] +[EntityRbac(typeof(RbacTest4), Verbs = RbacVerb.Get | RbacVerb.Update)] +public class RbacTest4 : Base +{ +} diff --git a/test/KubeOps.Transpiler.Test/TestEntities/TestSpecEntity.cs b/test/KubeOps.Transpiler.Test/TestEntities/TestSpecEntity.cs index 975a9781..356c54a5 100644 --- a/test/KubeOps.Transpiler.Test/TestEntities/TestSpecEntity.cs +++ b/test/KubeOps.Transpiler.Test/TestEntities/TestSpecEntity.cs @@ -1,187 +1,187 @@ -using System.Collections; -using System.Collections.ObjectModel; -using System.Text.Json.Serialization; - -using k8s; -using k8s.Models; - -using KubeOps.Abstractions.Entities; -using KubeOps.Abstractions.Entities.Attributes; - -namespace KubeOps.Transpiler.Test.TestEntities; - -[Description("This is the Spec Class Description")] -public class TestSpecEntitySpec -{ - public string[] StringArray { get; set; } = Array.Empty(); - - public string[]? NullableStringArray { get; set; } - - public IEnumerable EnumerableInteger { get; set; } = Array.Empty(); - - public IEnumerable EnumerableNullableInteger { get; set; } = Array.Empty(); - - public IntegerList IntegerList { get; set; } = new(); - - public HashSet IntegerHashSet { get; set; } = new(); - - public ISet IntegerISet { get; set; } = new HashSet(); - - public IReadOnlySet IntegerIReadOnlySet { get; set; } = new HashSet(); - - [AdditionalPrinterColumn] - public string NormalString { get; set; } = string.Empty; - - public string? NullableString { get; set; } - - [AdditionalPrinterColumn(PrinterColumnPriority.WideView)] - public int NormalInt { get; set; } - - public int? NullableInt { get; set; } - - [AdditionalPrinterColumn(name: "OtherName")] - public long NormalLong { get; set; } - - public long? NullableLong { get; set; } - - public float NormalFloat { get; set; } - - public float? NullableFloat { get; set; } - - public double NormalDouble { get; set; } - - public double? NullableDouble { get; set; } - - public bool NormalBool { get; set; } - - public bool? NullableBool { get; set; } - - public DateTime NormalDateTime { get; set; } - - public DateTime? NullableDateTime { get; set; } - - public TestSpecEnum NormalEnum { get; set; } - - public TestSpecEnum? NullableEnum { get; set; } - - [Description("Description")] - public string Description { get; set; } = string.Empty; - - [ExternalDocs("https://google.ch")] - public string ExternalDocs { get; set; } = string.Empty; - - [ExternalDocs("https://google.ch", "Description")] - public string ExternalDocsWithDescription { get; set; } = string.Empty; - - [Items(13, 42)] - public string[] Items { get; set; } = Array.Empty(); - - [Length(2, 42)] - public string Length { get; set; } = string.Empty; - - [MultipleOf(15)] - public int MultipleOf { get; set; } - - [Pattern(@"/\d*/")] - public string Pattern { get; set; } = string.Empty; - - [RangeMinimum(15, true)] - public int RangeMinimum { get; set; } - - [RangeMaximum(15, true)] - public int RangeMaximum { get; set; } - - [Required] - public int Required { get; set; } - - [Ignore] - public string IgnoredProperty { get; set; } = string.Empty; - - public IEnumerable ComplexItemsEnumerable { get; set; } = Enumerable.Empty(); - - public List ComplexItemsList { get; set; } = new(); - - public IList ComplexItemsIList { get; set; } = Array.Empty(); - - public IReadOnlyList ComplexItemsReadOnlyList { get; set; } = Array.Empty(); - - public Collection ComplexItemsCollection { get; set; } = new(); - - public ICollection ComplexItemsICollection { get; set; } = Array.Empty(); - - public IReadOnlyCollection ComplexItemsReadOnlyCollection { get; set; } = Array.Empty(); - - public TestItemList ComplexItemsDerivedList { get; set; } = new(); - - public IDictionary Dictionary { get; set; } = new Dictionary(); - - public IDictionary GenericDictionary { get; set; } = new Dictionary(); - public IDictionary NormalGenericDictionary { get; set; } = new Dictionary(); - public IDictionary? NullableGenericDictionary { get; set; } = new Dictionary(); - - public IEnumerable> KeyValueEnumerable { get; set; } = - new Dictionary(); - - public IEnumerable> NormalKeyValueEnumerable { get; set; } = - new Dictionary(); - - public IEnumerable>? NullableKeyValueEnumerable { get; set; } = - new Dictionary(); - - [PreserveUnknownFields] - public object PreserveUnknownFields { get; set; } = new object(); - - public IntstrIntOrString IntOrString { get; set; } = string.Empty; - - [EmbeddedResource] - public V1ConfigMap KubernetesObject { get; set; } = new V1ConfigMap(); - - public V1Pod Pod { get; set; } = new V1Pod(); - - public IList Pods { get; set; } = Array.Empty(); - - [JsonPropertyName("NameFromAttribute")] - public string PropertyWithJsonAttribute { get; set; } = string.Empty; - - public enum TestSpecEnum - { - Value1, - Value2, - } -} - -[KubernetesEntity(Group = "kubeops.test.dev", ApiVersion = "V1")] -[GenericAdditionalPrinterColumn(".metadata.namespace", "Namespace", "string")] -[GenericAdditionalPrinterColumn(".metadata.creationTimestamp", "Age", "date")] -public class TestSpecEntity : IKubernetesObject, ISpec -{ - public string ApiVersion { get; set; } = "kubeops.test.dev/v1"; - public string Kind { get; set; } = "TestSpecEntity"; - public V1ObjectMeta Metadata { get; set; } = new(); - public TestSpecEntitySpec Spec { get; set; } = new(); -} - -[KubernetesEntity(Group = "kubeops.test.dev", ApiVersion = "V1")] -[EntityScope(EntityScope.Cluster)] -public class TestClusterSpecEntity : IKubernetesObject, ISpec -{ - public string ApiVersion { get; set; } = "kubeops.test.dev/v1"; - public string Kind { get; set; } = "TestClusterSpecEntity"; - public V1ObjectMeta Metadata { get; set; } = new(); - public TestSpecEntitySpec Spec { get; set; } = new(); -} - -public class TestItem -{ - public string Name { get; set; } = null!; - public string Item { get; set; } = null!; - public string Extra { get; set; } = null!; -} - -public class TestItemList : List -{ -} - -public class IntegerList : Collection -{ -} +using System.Collections; +using System.Collections.ObjectModel; +using System.Text.Json.Serialization; + +using k8s; +using k8s.Models; + +using KubeOps.Abstractions.Entities; +using KubeOps.Abstractions.Entities.Attributes; + +namespace KubeOps.Transpiler.Test.TestEntities; + +[Description("This is the Spec Class Description")] +public class TestSpecEntitySpec +{ + public string[] StringArray { get; set; } = Array.Empty(); + + public string[]? NullableStringArray { get; set; } + + public IEnumerable EnumerableInteger { get; set; } = Array.Empty(); + + public IEnumerable EnumerableNullableInteger { get; set; } = Array.Empty(); + + public IntegerList IntegerList { get; set; } = new(); + + public HashSet IntegerHashSet { get; set; } = new(); + + public ISet IntegerISet { get; set; } = new HashSet(); + + public IReadOnlySet IntegerIReadOnlySet { get; set; } = new HashSet(); + + [AdditionalPrinterColumn] + public string NormalString { get; set; } = string.Empty; + + public string? NullableString { get; set; } + + [AdditionalPrinterColumn(PrinterColumnPriority.WideView)] + public int NormalInt { get; set; } + + public int? NullableInt { get; set; } + + [AdditionalPrinterColumn(name: "OtherName")] + public long NormalLong { get; set; } + + public long? NullableLong { get; set; } + + public float NormalFloat { get; set; } + + public float? NullableFloat { get; set; } + + public double NormalDouble { get; set; } + + public double? NullableDouble { get; set; } + + public bool NormalBool { get; set; } + + public bool? NullableBool { get; set; } + + public DateTime NormalDateTime { get; set; } + + public DateTime? NullableDateTime { get; set; } + + public TestSpecEnum NormalEnum { get; set; } + + public TestSpecEnum? NullableEnum { get; set; } + + [Description("Description")] + public string Description { get; set; } = string.Empty; + + [ExternalDocs("https://google.ch")] + public string ExternalDocs { get; set; } = string.Empty; + + [ExternalDocs("https://google.ch", "Description")] + public string ExternalDocsWithDescription { get; set; } = string.Empty; + + [Items(13, 42)] + public string[] Items { get; set; } = Array.Empty(); + + [Length(2, 42)] + public string Length { get; set; } = string.Empty; + + [MultipleOf(15)] + public int MultipleOf { get; set; } + + [Pattern(@"/\d*/")] + public string Pattern { get; set; } = string.Empty; + + [RangeMinimum(15, true)] + public int RangeMinimum { get; set; } + + [RangeMaximum(15, true)] + public int RangeMaximum { get; set; } + + [Required] + public int Required { get; set; } + + [Ignore] + public string IgnoredProperty { get; set; } = string.Empty; + + public IEnumerable ComplexItemsEnumerable { get; set; } = Enumerable.Empty(); + + public List ComplexItemsList { get; set; } = new(); + + public IList ComplexItemsIList { get; set; } = Array.Empty(); + + public IReadOnlyList ComplexItemsReadOnlyList { get; set; } = Array.Empty(); + + public Collection ComplexItemsCollection { get; set; } = new(); + + public ICollection ComplexItemsICollection { get; set; } = Array.Empty(); + + public IReadOnlyCollection ComplexItemsReadOnlyCollection { get; set; } = Array.Empty(); + + public TestItemList ComplexItemsDerivedList { get; set; } = new(); + + public IDictionary Dictionary { get; set; } = new Dictionary(); + + public IDictionary GenericDictionary { get; set; } = new Dictionary(); + public IDictionary NormalGenericDictionary { get; set; } = new Dictionary(); + public IDictionary? NullableGenericDictionary { get; set; } = new Dictionary(); + + public IEnumerable> KeyValueEnumerable { get; set; } = + new Dictionary(); + + public IEnumerable> NormalKeyValueEnumerable { get; set; } = + new Dictionary(); + + public IEnumerable>? NullableKeyValueEnumerable { get; set; } = + new Dictionary(); + + [PreserveUnknownFields] + public object PreserveUnknownFields { get; set; } = new object(); + + public IntstrIntOrString IntOrString { get; set; } = string.Empty; + + [EmbeddedResource] + public V1ConfigMap KubernetesObject { get; set; } = new V1ConfigMap(); + + public V1Pod Pod { get; set; } = new V1Pod(); + + public IList Pods { get; set; } = Array.Empty(); + + [JsonPropertyName("NameFromAttribute")] + public string PropertyWithJsonAttribute { get; set; } = string.Empty; + + public enum TestSpecEnum + { + Value1, + Value2, + } +} + +[KubernetesEntity(Group = "kubeops.test.dev", ApiVersion = "V1")] +[GenericAdditionalPrinterColumn(".metadata.namespace", "Namespace", "string")] +[GenericAdditionalPrinterColumn(".metadata.creationTimestamp", "Age", "date")] +public class TestSpecEntity : IKubernetesObject, ISpec +{ + public string ApiVersion { get; set; } = "kubeops.test.dev/v1"; + public string Kind { get; set; } = "TestSpecEntity"; + public V1ObjectMeta Metadata { get; set; } = new(); + public TestSpecEntitySpec Spec { get; set; } = new(); +} + +[KubernetesEntity(Group = "kubeops.test.dev", ApiVersion = "V1")] +[EntityScope(EntityScope.Cluster)] +public class TestClusterSpecEntity : IKubernetesObject, ISpec +{ + public string ApiVersion { get; set; } = "kubeops.test.dev/v1"; + public string Kind { get; set; } = "TestClusterSpecEntity"; + public V1ObjectMeta Metadata { get; set; } = new(); + public TestSpecEntitySpec Spec { get; set; } = new(); +} + +public class TestItem +{ + public string Name { get; set; } = null!; + public string Item { get; set; } = null!; + public string Extra { get; set; } = null!; +} + +public class TestItemList : List +{ +} + +public class IntegerList : Collection +{ +} diff --git a/test/KubeOps.Transpiler.Test/TestEntities/TestStatusEntity.cs b/test/KubeOps.Transpiler.Test/TestEntities/TestStatusEntity.cs index 9fcc61c9..7a03e1ba 100644 --- a/test/KubeOps.Transpiler.Test/TestEntities/TestStatusEntity.cs +++ b/test/KubeOps.Transpiler.Test/TestEntities/TestStatusEntity.cs @@ -1,35 +1,35 @@ -using k8s; -using k8s.Models; - -using KubeOps.Abstractions.Entities.Attributes; - -namespace KubeOps.Transpiler.Test.TestEntities; - -public class TestStatusEntitySpec -{ - public string SpecString { get; set; } = string.Empty; -} - -public class TestStatusEntityStatus -{ - public string StatusString { get; set; } = string.Empty; - public List StatusList { get; set; } = new(); -} - -public class ComplexStatusObject -{ - public string ObjectName { get; set; } = string.Empty; - public DateTime LastModified { get; set; } -} - -[KubernetesEntity(Group = "kubeops.test.dev", ApiVersion = "V1")] -[KubernetesEntityShortNames("foo", "bar", "baz")] -public class TestStatusEntity : IKubernetesObject, ISpec, - IStatus -{ - public string ApiVersion { get; set; } = "kubeops.test.dev/v1"; - public string Kind { get; set; } = "TestStatusEntity"; - public V1ObjectMeta Metadata { get; set; } = new(); - public TestStatusEntitySpec Spec { get; set; } = new(); - public TestStatusEntityStatus Status { get; set; } = new(); -} +using k8s; +using k8s.Models; + +using KubeOps.Abstractions.Entities.Attributes; + +namespace KubeOps.Transpiler.Test.TestEntities; + +public class TestStatusEntitySpec +{ + public string SpecString { get; set; } = string.Empty; +} + +public class TestStatusEntityStatus +{ + public string StatusString { get; set; } = string.Empty; + public List StatusList { get; set; } = new(); +} + +public class ComplexStatusObject +{ + public string ObjectName { get; set; } = string.Empty; + public DateTime LastModified { get; set; } +} + +[KubernetesEntity(Group = "kubeops.test.dev", ApiVersion = "V1")] +[KubernetesEntityShortNames("foo", "bar", "baz")] +public class TestStatusEntity : IKubernetesObject, ISpec, + IStatus +{ + public string ApiVersion { get; set; } = "kubeops.test.dev/v1"; + public string Kind { get; set; } = "TestStatusEntity"; + public V1ObjectMeta Metadata { get; set; } = new(); + public TestStatusEntitySpec Spec { get; set; } = new(); + public TestStatusEntityStatus Status { get; set; } = new(); +} diff --git a/test/KubeOps.Transpiler.Test/TestEntities/VersionedEntities.cs b/test/KubeOps.Transpiler.Test/TestEntities/VersionedEntities.cs index a4cdd77b..68dd8bfa 100644 --- a/test/KubeOps.Transpiler.Test/TestEntities/VersionedEntities.cs +++ b/test/KubeOps.Transpiler.Test/TestEntities/VersionedEntities.cs @@ -1,69 +1,69 @@ -using k8s.Models; - -using KubeOps.Abstractions.Entities.Attributes; - -namespace KubeOps.Transpiler.Test.TestEntities; - -[KubernetesEntity( - ApiVersion = "v1alpha1", - Kind = "VersionedEntity", - Group = "kubeops.test.dev", - PluralName = "versionedentities")] -public class V1Alpha1VersionedEntity : Base -{ -} - -[KubernetesEntity( - ApiVersion = "v1beta1", - Kind = "VersionedEntity", - Group = "kubeops.test.dev", - PluralName = "versionedentities")] -public class V1Beta1VersionedEntity : Base -{ -} - -[KubernetesEntity( - ApiVersion = "v2beta2", - Kind = "VersionedEntity", - Group = "kubeops.test.dev", - PluralName = "versionedentities")] -public class V2Beta2VersionedEntity : Base -{ -} - -[KubernetesEntity( - ApiVersion = "v2", - Kind = "VersionedEntity", - Group = "kubeops.test.dev", - PluralName = "versionedentities")] -public class V2VersionedEntity : Base -{ -} - -[KubernetesEntity( - ApiVersion = "v1", - Kind = "VersionedEntity", - Group = "kubeops.test.dev", - PluralName = "versionedentities")] -public class V1VersionedEntity : Base -{ -} - -[KubernetesEntity( - ApiVersion = "v1", - Kind = "AttributeVersionedEntity", - Group = "kubeops.test.dev", - PluralName = "attributeversionedentities")] -[StorageVersion] -public class V1AttributeVersionedEntity : Base -{ -} - -[KubernetesEntity( - ApiVersion = "v2", - Kind = "AttributeVersionedEntity", - Group = "kubeops.test.dev", - PluralName = "attributeversionedentities")] -public class V2AttributeVersionedEntity : Base -{ -} +using k8s.Models; + +using KubeOps.Abstractions.Entities.Attributes; + +namespace KubeOps.Transpiler.Test.TestEntities; + +[KubernetesEntity( + ApiVersion = "v1alpha1", + Kind = "VersionedEntity", + Group = "kubeops.test.dev", + PluralName = "versionedentities")] +public class V1Alpha1VersionedEntity : Base +{ +} + +[KubernetesEntity( + ApiVersion = "v1beta1", + Kind = "VersionedEntity", + Group = "kubeops.test.dev", + PluralName = "versionedentities")] +public class V1Beta1VersionedEntity : Base +{ +} + +[KubernetesEntity( + ApiVersion = "v2beta2", + Kind = "VersionedEntity", + Group = "kubeops.test.dev", + PluralName = "versionedentities")] +public class V2Beta2VersionedEntity : Base +{ +} + +[KubernetesEntity( + ApiVersion = "v2", + Kind = "VersionedEntity", + Group = "kubeops.test.dev", + PluralName = "versionedentities")] +public class V2VersionedEntity : Base +{ +} + +[KubernetesEntity( + ApiVersion = "v1", + Kind = "VersionedEntity", + Group = "kubeops.test.dev", + PluralName = "versionedentities")] +public class V1VersionedEntity : Base +{ +} + +[KubernetesEntity( + ApiVersion = "v1", + Kind = "AttributeVersionedEntity", + Group = "kubeops.test.dev", + PluralName = "attributeversionedentities")] +[StorageVersion] +public class V1AttributeVersionedEntity : Base +{ +} + +[KubernetesEntity( + ApiVersion = "v2", + Kind = "AttributeVersionedEntity", + Group = "kubeops.test.dev", + PluralName = "attributeversionedentities")] +public class V2AttributeVersionedEntity : Base +{ +}