From 42bc8eea5b8f110d0c4203eec9c06a81e2a306e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BChler?= Date: Fri, 29 Sep 2023 17:05:18 +0200 Subject: [PATCH] feat(client): Add Kubernetes Client package BREAKING CHANGE: The IKubernetesClient interface and implementation now require the TEntity typeparam instead of each method providing one. The implementation is instanced with EntityMetadata to allow the operator to inject the clients for each entity. --- KubeOps.sln | 14 + .../Builder/IOperatorBuilder.cs | 6 +- .../Entities/EntityList.cs | 22 ++ .../Entities/Extensions.cs | 42 +++ .../EntityDefinitionGenerator.cs | 4 +- .../AttributedEntity.cs | 20 +- .../KubernetesEntitySyntaxReceiver.cs | 78 ++-- .../IKubernetesClient.cs | 96 ++--- .../KubernetesClient.cs | 332 +++++++++--------- .../LabelSelectors/EqualsSelector.cs | 12 +- .../LabelSelectors/ExistsSelector.cs | 5 +- .../LabelSelectors/Extensions.cs | 6 +- .../LabelSelectors/ILabelSelector.cs | 13 - .../LabelSelectors/LabelSelector.cs | 20 ++ .../LabelSelectors/NotEqualsSelector.cs | 12 +- .../LabelSelectors/NotExistsSelector.cs | 5 +- .../Builder/OperatorBuilder.cs | 13 +- .../Client/IKubernetesClientFactory.cs | 19 - .../Client/KubernetesClientFactory.cs | 45 --- src/KubeOps.Operator/KubeOps.Operator.csproj | 1 + .../Watcher/ResourceWatcher{TEntity}.cs | 10 +- .../GlobalUsings.cs | 1 + .../KubeOps.KubernetesClient.Test.csproj | 5 + .../KubernetesClient.Test.cs | 138 ++++++++ 24 files changed, 523 insertions(+), 396 deletions(-) create mode 100644 src/KubeOps.Abstractions/Entities/EntityList.cs create mode 100644 src/KubeOps.Abstractions/Entities/Extensions.cs rename src/KubeOps.Generator/{EntityDefinitions => SyntaxReceiver}/AttributedEntity.cs (77%) rename src/KubeOps.Generator/{EntityDefinitions => SyntaxReceiver}/KubernetesEntitySyntaxReceiver.cs (94%) delete mode 100644 src/KubeOps.KubernetesClient/LabelSelectors/ILabelSelector.cs create mode 100644 src/KubeOps.KubernetesClient/LabelSelectors/LabelSelector.cs delete mode 100644 src/KubeOps.Operator/Client/IKubernetesClientFactory.cs delete mode 100644 src/KubeOps.Operator/Client/KubernetesClientFactory.cs create mode 100644 test/KubeOps.KubernetesClient.Test/GlobalUsings.cs create mode 100644 test/KubeOps.KubernetesClient.Test/KubeOps.KubernetesClient.Test.csproj create mode 100644 test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs diff --git a/KubeOps.sln b/KubeOps.sln index c121685e..fa42e10c 100644 --- a/KubeOps.sln +++ b/KubeOps.sln @@ -47,6 +47,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeOps.Transpiler.Test", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeOps.Transpiler", "src\KubeOps.Transpiler\KubeOps.Transpiler.csproj", "{A793FC08-E76C-448B-BE93-88C137D2C7AB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeOps.KubernetesClient", "src\KubeOps.KubernetesClient\KubeOps.KubernetesClient.csproj", "{C2C6FF06-2B9D-4FAC-A039-3DC1E007DE3B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeOps.KubernetesClient.Test", "test\KubeOps.KubernetesClient.Test\KubeOps.KubernetesClient.Test.csproj", "{25F767E5-7A74-459B-83CC-39519461F38B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -68,6 +72,8 @@ Global {BC6E6D3C-6404-4B01-9D72-AA16FEEBFF5C} = {C587731F-8191-4A19-8662-B89A60FE79A1} {47914451-147D-427E-B150-9C47DBF28F2C} = {C587731F-8191-4A19-8662-B89A60FE79A1} {A793FC08-E76C-448B-BE93-88C137D2C7AB} = {4DB01062-6DC5-4028-BB72-C0619C2F5F2E} + {C2C6FF06-2B9D-4FAC-A039-3DC1E007DE3B} = {4DB01062-6DC5-4028-BB72-C0619C2F5F2E} + {25F767E5-7A74-459B-83CC-39519461F38B} = {C587731F-8191-4A19-8662-B89A60FE79A1} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {E9A0B04E-D90E-4B94-90E0-DD3666B098FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -118,5 +124,13 @@ Global {A793FC08-E76C-448B-BE93-88C137D2C7AB}.Debug|Any CPU.Build.0 = Debug|Any CPU {A793FC08-E76C-448B-BE93-88C137D2C7AB}.Release|Any CPU.ActiveCfg = Release|Any CPU {A793FC08-E76C-448B-BE93-88C137D2C7AB}.Release|Any CPU.Build.0 = Release|Any CPU + {C2C6FF06-2B9D-4FAC-A039-3DC1E007DE3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2C6FF06-2B9D-4FAC-A039-3DC1E007DE3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2C6FF06-2B9D-4FAC-A039-3DC1E007DE3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2C6FF06-2B9D-4FAC-A039-3DC1E007DE3B}.Release|Any CPU.Build.0 = Release|Any CPU + {25F767E5-7A74-459B-83CC-39519461F38B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25F767E5-7A74-459B-83CC-39519461F38B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25F767E5-7A74-459B-83CC-39519461F38B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25F767E5-7A74-459B-83CC-39519461F38B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/KubeOps.Abstractions/Builder/IOperatorBuilder.cs b/src/KubeOps.Abstractions/Builder/IOperatorBuilder.cs index 649a98a8..bfba5682 100644 --- a/src/KubeOps.Abstractions/Builder/IOperatorBuilder.cs +++ b/src/KubeOps.Abstractions/Builder/IOperatorBuilder.cs @@ -19,14 +19,14 @@ public interface IOperatorBuilder IServiceCollection Services { get; } /// - /// Add metadata for an entity to the operator. + /// Add an entity with its metadata to the operator. /// Metadata must be added for each entity to be used in /// controllers and other elements. /// /// The metadata of the entity. /// The type of the entity. /// The builder for chaining. - IOperatorBuilder AddEntityMetadata(EntityMetadata metadata) + IOperatorBuilder AddEntity(EntityMetadata metadata) where TEntity : IKubernetesObject; /// @@ -48,7 +48,7 @@ IOperatorBuilder AddController() /// Implementation type of the controller. /// Entity type. /// The builder for chaining. - IOperatorBuilder AddController(EntityMetadata metadata) + IOperatorBuilder AddControllerWithEntity(EntityMetadata metadata) where TImplementation : class, IEntityController where TEntity : IKubernetesObject; } diff --git a/src/KubeOps.Abstractions/Entities/EntityList.cs b/src/KubeOps.Abstractions/Entities/EntityList.cs new file mode 100644 index 00000000..5071e7eb --- /dev/null +++ b/src/KubeOps.Abstractions/Entities/EntityList.cs @@ -0,0 +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(); +} diff --git a/src/KubeOps.Abstractions/Entities/Extensions.cs b/src/KubeOps.Abstractions/Entities/Extensions.cs new file mode 100644 index 00000000..06045853 --- /dev/null +++ b/src/KubeOps.Abstractions/Entities/Extensions.cs @@ -0,0 +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; + } +} diff --git a/src/KubeOps.Generator/EntityDefinitions/EntityDefinitionGenerator.cs b/src/KubeOps.Generator/EntityDefinitions/EntityDefinitionGenerator.cs index 5eceb383..240b7a7e 100644 --- a/src/KubeOps.Generator/EntityDefinitions/EntityDefinitionGenerator.cs +++ b/src/KubeOps.Generator/EntityDefinitions/EntityDefinitionGenerator.cs @@ -1,3 +1,5 @@ +using KubeOps.Generator.SyntaxReceiver; + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -93,7 +95,7 @@ public void Execute(GeneratorExecutionContext context) MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, IdentifierName("builder"), - GenericName(Identifier("AddEntityMetadata")) + GenericName(Identifier("AddEntity")) .WithTypeArgumentList( TypeArgumentList( SingletonSeparatedList( diff --git a/src/KubeOps.Generator/EntityDefinitions/AttributedEntity.cs b/src/KubeOps.Generator/SyntaxReceiver/AttributedEntity.cs similarity index 77% rename from src/KubeOps.Generator/EntityDefinitions/AttributedEntity.cs rename to src/KubeOps.Generator/SyntaxReceiver/AttributedEntity.cs index b8758231..58674eff 100644 --- a/src/KubeOps.Generator/EntityDefinitions/AttributedEntity.cs +++ b/src/KubeOps.Generator/SyntaxReceiver/AttributedEntity.cs @@ -1,10 +1,10 @@ -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace KubeOps.Generator.EntityDefinitions; - -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/EntityDefinitions/KubernetesEntitySyntaxReceiver.cs b/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs similarity index 94% rename from src/KubeOps.Generator/EntityDefinitions/KubernetesEntitySyntaxReceiver.cs rename to src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs index 2eaa0218..37cfe3ac 100644 --- a/src/KubeOps.Generator/EntityDefinitions/KubernetesEntitySyntaxReceiver.cs +++ b/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs @@ -1,38 +1,40 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace KubeOps.Generator.EntityDefinitions; - -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 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; +} diff --git a/src/KubeOps.KubernetesClient/IKubernetesClient.cs b/src/KubeOps.KubernetesClient/IKubernetesClient.cs index 3476667d..68a966c5 100644 --- a/src/KubeOps.KubernetesClient/IKubernetesClient.cs +++ b/src/KubeOps.KubernetesClient/IKubernetesClient.cs @@ -1,21 +1,19 @@ 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 . /// -public interface IKubernetesClient +/// The type of the Kubernetes entity. +public interface IKubernetesClient + where TEntity : IKubernetesObject { - /// - /// Represents the "original" kubernetes client from the - /// "KubernetesClient" package. - /// - IKubernetes ApiClient { get; } - /// /// Return the base URI of the currently used KubernetesClient. /// @@ -50,35 +48,25 @@ public interface IKubernetesClient Task GetCurrentNamespace(string downwardApiEnvName = "POD_NAMESPACE"); /// - /// Fetch and return the actual Kubernetes (aka. Server Version). - /// - /// The of the current server. - Task GetServerVersion(); - - /// - /// Fetch and return a entity from the Kubernetes API. + /// 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 concrete type of the entity. /// The found entity of the given type, or null otherwise. - Task Get(string name, string? @namespace = null) - where TEntity : class, IKubernetesObject; + 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. - /// The concrete type of the entity. /// A list of Kubernetes entities. - Task> List( + Task> List( string? @namespace = null, - string? labelSelector = null) - where TEntity : IKubernetesObject; + string? labelSelector = null); /// /// Fetch and return a list of entities from the Kubernetes API. @@ -87,93 +75,78 @@ Task> List( /// If only entities in a given namespace should be listed, provide the namespace here. /// /// A list of label-selectors to apply to the search. - /// The concrete type of the entity. /// A list of Kubernetes entities. - Task> List( + Task> List( string? @namespace = null, - params ILabelSelector[] labelSelectors) - where TEntity : IKubernetesObject; + 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 concrete type of the entity. /// The saved instance of the entity. - Task Save(TEntity entity) - where TEntity : class, IKubernetesObject; + 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 concrete type of the entity. /// The created instance of the entity. - Task Create(TEntity entity) - where TEntity : IKubernetesObject; + Task Create(TEntity entity); /// /// Update the given entity on the Kubernetes API. /// /// The entity instance. - /// The concrete type of the entity. /// The updated instance of the entity. - Task Update(TEntity entity) - where TEntity : IKubernetesObject; + Task Update(TEntity entity); /// /// Update the status object of a given entity on the Kubernetes API. /// /// The entity that contains a status object. - /// The concrete type of the entity. /// A task that completes when the call was made. - public Task UpdateStatus(TEntity entity) - where TEntity : IKubernetesObject; + public Task UpdateStatus(TEntity entity); /// /// Delete a given entity from the Kubernetes API. /// /// The entity in question. - /// The concrete type of the entity. /// A task that completes when the call was made. - Task Delete(TEntity entity) - where TEntity : IKubernetesObject; + Task Delete(TEntity entity); /// /// Delete a given list of entities from the Kubernetes API. /// /// The entities in question. - /// The concrete type of the entity. /// A task that completes when the calls were made. - Task Delete(IEnumerable entities) - where TEntity : IKubernetesObject; + Task Delete(IEnumerable entities); /// /// Delete a given list of entities from the Kubernetes API. /// /// The entities in question. - /// The concrete type of the entity. /// A task that completes when the calls were made. - Task Delete(params TEntity[] entities) - where TEntity : IKubernetesObject; + 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. - /// The concrete type of the entity. /// A task that completes when the call was made. - Task Delete(string name, string? @namespace = null) - where TEntity : IKubernetesObject; + 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: . /// - /// The timeout which the watcher has (after this timeout, the server will close the connection). /// Action that is called when an event occurs. /// Action that handles exceptions. /// Action that handles closed connections. @@ -181,26 +154,24 @@ Task Delete(string name, string? @namespace = null) /// 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. - /// The concrete type of the entity. /// A entity watcher for the given entity. - Task> Watch( - TimeSpan timeout, + Watcher Watch( Action onEvent, Action? onError = null, Action? onClose = null, string? @namespace = null, + TimeSpan? timeout = null, CancellationToken cancellationToken = default, - params ILabelSelector[] labelSelectors) - where TEntity : IKubernetesObject; + params LabelSelector[] labelSelectors); /// /// Create a entity watcher on the Kubernetes API. /// The entity watcher fires events for entity-events on /// Kubernetes (events: . /// - /// The timeout which the watcher has (after this timeout, the server will close the connection). /// Action that is called when an event occurs. /// Action that handles exceptions. /// Action that handles closed connections. @@ -208,17 +179,16 @@ Task> Watch( /// The namespace to watch for entities (if needed). /// If the namespace is omitted, all entities on the cluster are watched. /// - /// Cancellation-Token. + /// 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. - /// The concrete type of the entity. + /// Cancellation-Token. /// A entity watcher for the given entity. - Task> Watch( - TimeSpan timeout, + Watcher Watch( Action onEvent, Action? onError = null, Action? onClose = null, string? @namespace = null, - CancellationToken cancellationToken = default, - string? labelSelector = null) - where TEntity : IKubernetesObject; + TimeSpan? timeout = null, + string? labelSelector = null, + CancellationToken cancellationToken = default); } diff --git a/src/KubeOps.KubernetesClient/KubernetesClient.cs b/src/KubeOps.KubernetesClient/KubernetesClient.cs index 5afc0ba6..1840e546 100644 --- a/src/KubeOps.KubernetesClient/KubernetesClient.cs +++ b/src/KubeOps.KubernetesClient/KubernetesClient.cs @@ -4,263 +4,259 @@ using k8s.Autorest; using k8s.Models; +using KubeOps.Abstractions.Entities; using KubeOps.KubernetesClient.LabelSelectors; namespace KubeOps.KubernetesClient; -public class KubernetesClient : IKubernetesClient +/// +public class KubernetesClient : IKubernetesClient + 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; - - public KubernetesClient() - : this(KubernetesClientConfiguration.BuildDefaultConfig()) + 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()) { } - public KubernetesClient(KubernetesClientConfiguration clientConfig) - : this(clientConfig, new Kubernetes(clientConfig)) + /// + /// 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)) { } - public KubernetesClient(KubernetesClientConfiguration clientConfig, IKubernetes client) + /// + /// 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), + _ => new GenericClient( + client, + metadata.Group, + metadata.Version, + metadata.PluralName), + }; } - /// - public IKubernetes ApiClient => _client; - /// public Uri BaseUri => _client.BaseUri; /// - public Task GetCurrentNamespace(string downwardApiEnvName = "POD_NAMESPACE") + public async Task GetCurrentNamespace(string downwardApiEnvName = "POD_NAMESPACE") { - var result = DefaultNamespace; - - if (_clientConfig.Namespace != null) + if (_clientConfig.Namespace is { } configValue) { - result = _clientConfig.Namespace; + return configValue; } - if (Environment.GetEnvironmentVariable(downwardApiEnvName) != null) + if (Environment.GetEnvironmentVariable(downwardApiEnvName) is { } envValue) { - result = Environment.GetEnvironmentVariable(downwardApiEnvName) ?? string.Empty; + return envValue; } if (File.Exists(DownwardApiNamespaceFile)) { - var ns = File.ReadAllText(DownwardApiNamespaceFile); - result = ns.Trim(); + var ns = await File.ReadAllTextAsync(DownwardApiNamespaceFile); + return ns.Trim(); } - return Task.FromResult(result); + return DefaultNamespace; } /// - public Task GetServerVersion() => _client.Version.GetCodeAsync(); - - /// - public async Task Get( - string name, - string? @namespace = null) - where TResource : class, IKubernetesObject + public async Task Get(string name, string? @namespace = null) { - try + var list = @namespace switch { - var client = CreateClient(); + 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 await (string.IsNullOrWhiteSpace(@namespace) - ? client.ReadAsync(name) - : client.ReadNamespacedAsync(@namespace, name)); - } - catch (HttpOperationException e) when (e.Response.StatusCode == HttpStatusCode.NotFound) + return list switch { - return null; - } + { Items: [var existing] } => existing, + _ => default, + }; } /// - public async Task> List(string? @namespace = null, string? labelSelector = null) - where TResource : IKubernetesObject - { - var definition = EntityDefinition.FromType(); - var result = await (string.IsNullOrWhiteSpace(@namespace) - ? _client.CustomObjects.ListClusterCustomObjectWithHttpMessagesAsync( - definition.Group, - definition.Version, - definition.Plural, - labelSelector: labelSelector) - : _client.CustomObjects.ListNamespacedCustomObjectWithHttpMessagesAsync( - definition.Group, - definition.Version, + 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, - definition.Plural, - labelSelector: labelSelector)); - var list = KubernetesJson.Deserialize>(result.Body.ToString()); - return list.Items; - } - - /// - public Task> List( - string? @namespace = null, - params ILabelSelector[] labelSelectors) - where TResource : IKubernetesObject => - List(@namespace, labelSelectors.ToExpression()); + _metadata.PluralName, + labelSelector: labelSelector), + }).Items; /// - public Task Save(TResource resource) - where TResource : class, IKubernetesObject => - resource.Uid() is null - ? Create(resource) - : Update(resource); + public Task> List(string? @namespace = null, params LabelSelector[] labelSelectors) + => List(@namespace, labelSelectors.ToExpression()); /// - public async Task Create(TResource resource) - where TResource : IKubernetesObject - { - var client = CreateClient(); - - return await (string.IsNullOrWhiteSpace(resource.Namespace()) - ? client.CreateAsync(resource) - : client.CreateNamespacedAsync(resource, resource.Namespace())); - } + public Task Create(TEntity entity) + => entity.Namespace() switch + { + { } ns => _genericClient.CreateNamespacedAsync(entity, ns), + null => _genericClient.CreateAsync(entity), + }; /// - public async Task Update(TResource resource) - where TResource : IKubernetesObject - { - var client = CreateClient(); - - return await (string.IsNullOrWhiteSpace(resource.Namespace()) - ? client.ReplaceAsync(resource, resource.Name()) - : client.ReplaceNamespacedAsync(resource, resource.Namespace(), resource.Name())); - } + public Task Update(TEntity entity) + => entity.Namespace() switch + { + { } ns => _genericClient.ReplaceNamespacedAsync(entity, ns, entity.Name()), + null => _genericClient.ReplaceAsync(entity, entity.Name()), + }; /// - public async Task UpdateStatus(TResource resource) - where TResource : IKubernetesObject - { - var definition = EntityDefinition.FromType(); - await (string.IsNullOrWhiteSpace(resource.Namespace()) - ? _client.CustomObjects.ReplaceClusterCustomObjectStatusAsync( - resource, - definition.Group, - definition.Version, - definition.Plural, - resource.Name()) - : _client.CustomObjects.ReplaceNamespacedCustomObjectStatusAsync( - resource, - definition.Group, - definition.Version, - resource.Namespace(), - definition.Plural, - resource.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(TResource resource) - where TResource : IKubernetesObject => Delete( - resource.Name(), - resource.Namespace()); + public Task Delete(TEntity entity) => Delete( + entity.Name(), + entity.Namespace()); /// - public Task Delete(IEnumerable resources) - where TResource : IKubernetesObject => - Task.WhenAll(resources.Select(Delete)); + public Task Delete(IEnumerable entities) => + Task.WhenAll(entities.Select(Delete)); /// - public Task Delete(params TResource[] resources) - where TResource : IKubernetesObject => - Task.WhenAll(resources.Select(Delete)); + public Task Delete(params TEntity[] entities) => + Task.WhenAll(entities.Select(Delete)); /// - public async Task Delete(string name, string? @namespace = null) - where TResource : IKubernetesObject + public async Task Delete(string name, string? @namespace = null) { - var client = CreateClient(); - try { - await (string.IsNullOrWhiteSpace(@namespace) - ? client.DeleteAsync(name) - : client.DeleteNamespacedAsync(@namespace, name)); + 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 Task> Watch( - TimeSpan timeout, - Action onEvent, + public Watcher Watch( + Action onEvent, Action? onError = null, Action? onClose = null, string? @namespace = null, + TimeSpan? timeout = null, CancellationToken cancellationToken = default, - params ILabelSelector[] labelSelectors) - where TResource : IKubernetesObject + params LabelSelector[] labelSelectors) => Watch( - timeout, onEvent, onError, onClose, @namespace, - cancellationToken, - string.Join(",", labelSelectors.Select(l => l.ToExpression()))); + timeout, + labelSelectors.ToExpression(), + cancellationToken); /// - public Task> Watch( - TimeSpan timeout, - Action onEvent, + public Watcher Watch( + Action onEvent, Action? onError = null, Action? onClose = null, string? @namespace = null, - CancellationToken cancellationToken = default, - string? labelSelector = null) - where TResource : IKubernetesObject - { - var crd = EntityDefinition.FromType(); - var result = string.IsNullOrWhiteSpace(@namespace) - ? _client.CustomObjects.ListClusterCustomObjectWithHttpMessagesAsync( - crd.Group, - crd.Version, - crd.Plural, + 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: (int)timeout.TotalSeconds, + timeoutSeconds: timeout switch + { + null => null, + _ => (int?)timeout.Value.TotalSeconds, + }, watch: true, - cancellationToken: cancellationToken) - : _client.CustomObjects.ListNamespacedCustomObjectWithHttpMessagesAsync( - crd.Group, - crd.Version, - @namespace, - crd.Plural, + cancellationToken: cancellationToken), + _ => _client.CustomObjects.ListClusterCustomObjectWithHttpMessagesAsync( + _metadata.Group ?? string.Empty, + _metadata.Version, + _metadata.PluralName, labelSelector: labelSelector, - timeoutSeconds: (int)timeout.TotalSeconds, + timeoutSeconds: timeout switch + { + null => null, + _ => (int?)timeout.Value.TotalSeconds, + }, watch: true, - cancellationToken: cancellationToken); - - return Task.FromResult( - result.Watch( - onEvent, - onError, - onClose)); - } - - private GenericClient CreateClient() - where TResource : IKubernetesObject - { - var definition = EntityDefinition.FromType(); - return definition.Group switch - { - "" => new GenericClient(_client, definition.Version, definition.Plural), - _ => new GenericClient(_client, definition.Group, definition.Version, definition.Plural), - }; - } + cancellationToken: cancellationToken), + }).Watch(onEvent, onError, onClose); } diff --git a/src/KubeOps.KubernetesClient/LabelSelectors/EqualsSelector.cs b/src/KubeOps.KubernetesClient/LabelSelectors/EqualsSelector.cs index ca6aacae..0e313b2f 100644 --- a/src/KubeOps.KubernetesClient/LabelSelectors/EqualsSelector.cs +++ b/src/KubeOps.KubernetesClient/LabelSelectors/EqualsSelector.cs @@ -5,13 +5,9 @@ /// a specific value (out of a list of values). /// Note that "label in (value)" is the same as "label == value". /// -public record EqualsSelector : ILabelSelector +/// The label that needs to equal to one of the values. +/// The possible values. +public record EqualsSelector(string Label, params string[] Values) : LabelSelector { - public EqualsSelector(string label, params string[] values) => (Label, Values) = (label, values); - - public string Label { get; } - - public IEnumerable Values { get; } - - public string ToExpression() => $"{Label} in ({string.Join(",", Values)})"; + 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 4f7c8305..ed40e73c 100644 --- a/src/KubeOps.KubernetesClient/LabelSelectors/ExistsSelector.cs +++ b/src/KubeOps.KubernetesClient/LabelSelectors/ExistsSelector.cs @@ -3,7 +3,8 @@ /// /// Selector that checks if a certain label exists. /// -public record ExistsSelector(string Label) : ILabelSelector +/// The label that needs to exist on the entity/resource. +public record ExistsSelector(string Label) : LabelSelector { - public string ToExpression() => $"{Label}"; + protected override string ToExpression() => $"{Label}"; } diff --git a/src/KubeOps.KubernetesClient/LabelSelectors/Extensions.cs b/src/KubeOps.KubernetesClient/LabelSelectors/Extensions.cs index a9689914..f22e7b63 100644 --- a/src/KubeOps.KubernetesClient/LabelSelectors/Extensions.cs +++ b/src/KubeOps.KubernetesClient/LabelSelectors/Extensions.cs @@ -3,10 +3,10 @@ public static class Extensions { /// - /// Convert an enumerable list of s to a string. + /// 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.Select(s => s.ToExpression())); + public static string ToExpression(this IEnumerable selectors) => + string.Join(",", selectors); } diff --git a/src/KubeOps.KubernetesClient/LabelSelectors/ILabelSelector.cs b/src/KubeOps.KubernetesClient/LabelSelectors/ILabelSelector.cs deleted file mode 100644 index e5524824..00000000 --- a/src/KubeOps.KubernetesClient/LabelSelectors/ILabelSelector.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace KubeOps.KubernetesClient.LabelSelectors; - -/// -/// Different label selectors for querying the Kubernetes API. -/// -public interface ILabelSelector -{ - /// - /// Create an expression from the label selector. - /// - /// A string that represents the label selector. - string ToExpression(); -} diff --git a/src/KubeOps.KubernetesClient/LabelSelectors/LabelSelector.cs b/src/KubeOps.KubernetesClient/LabelSelectors/LabelSelector.cs new file mode 100644 index 00000000..543b1c03 --- /dev/null +++ b/src/KubeOps.KubernetesClient/LabelSelectors/LabelSelector.cs @@ -0,0 +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(); +} diff --git a/src/KubeOps.KubernetesClient/LabelSelectors/NotEqualsSelector.cs b/src/KubeOps.KubernetesClient/LabelSelectors/NotEqualsSelector.cs index b78505aa..32a68d09 100644 --- a/src/KubeOps.KubernetesClient/LabelSelectors/NotEqualsSelector.cs +++ b/src/KubeOps.KubernetesClient/LabelSelectors/NotEqualsSelector.cs @@ -5,13 +5,9 @@ /// a specific value (out of a list of values). /// Note that "label notin (value)" is the same as "label != value". /// -public record NotEqualsSelector : ILabelSelector +/// The label that must not equal to one of the values. +/// The possible values. +public record NotEqualsSelector(string Label, params string[] Values) : LabelSelector { - public NotEqualsSelector(string label, params string[] values) => (Label, Values) = (label, values); - - public string Label { get; } - - public IEnumerable Values { get; } - - public string ToExpression() => $"{Label} notin ({string.Join(",", Values)})"; + 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 a047f91a..52db320a 100644 --- a/src/KubeOps.KubernetesClient/LabelSelectors/NotExistsSelector.cs +++ b/src/KubeOps.KubernetesClient/LabelSelectors/NotExistsSelector.cs @@ -3,7 +3,8 @@ /// /// Selector that checks if a certain label does not exist. /// -public record NotExistsSelector(string Label) : ILabelSelector +/// The label that must not exist on the entity/resource. +public record NotExistsSelector(string Label) : LabelSelector { - public string ToExpression() => $"!{Label}"; + protected override string ToExpression() => $"!{Label}"; } diff --git a/src/KubeOps.Operator/Builder/OperatorBuilder.cs b/src/KubeOps.Operator/Builder/OperatorBuilder.cs index f82bbd75..624524d4 100644 --- a/src/KubeOps.Operator/Builder/OperatorBuilder.cs +++ b/src/KubeOps.Operator/Builder/OperatorBuilder.cs @@ -4,7 +4,7 @@ using KubeOps.Abstractions.Builder; using KubeOps.Abstractions.Controller; using KubeOps.Abstractions.Entities; -using KubeOps.Operator.Client; +using KubeOps.KubernetesClient; using KubeOps.Operator.Watcher; using Microsoft.Extensions.DependencyInjection; @@ -13,20 +13,17 @@ namespace KubeOps.Operator.Builder; internal class OperatorBuilder : IOperatorBuilder { - private readonly IKubernetesClientFactory _entityClientFactory = new KubernetesClientFactory(); - public OperatorBuilder(IServiceCollection services) { Services = services; - Services.AddSingleton(_entityClientFactory); } public IServiceCollection Services { get; } - public IOperatorBuilder AddEntityMetadata(EntityMetadata metadata) + public IOperatorBuilder AddEntity(EntityMetadata metadata) where TEntity : IKubernetesObject { - _entityClientFactory.RegisterMetadata(metadata); + Services.AddSingleton>(new KubernetesClient(metadata)); return this; } @@ -39,8 +36,8 @@ public IOperatorBuilder AddController() return this; } - public IOperatorBuilder AddController(EntityMetadata metadata) + public IOperatorBuilder AddControllerWithEntity(EntityMetadata metadata) where TImplementation : class, IEntityController where TEntity : IKubernetesObject => - AddController().AddEntityMetadata(metadata); + AddController().AddEntity(metadata); } diff --git a/src/KubeOps.Operator/Client/IKubernetesClientFactory.cs b/src/KubeOps.Operator/Client/IKubernetesClientFactory.cs deleted file mode 100644 index d47399ac..00000000 --- a/src/KubeOps.Operator/Client/IKubernetesClientFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Immutable; - -using k8s; -using k8s.Models; - -using KubeOps.Abstractions.Entities; - -namespace KubeOps.Operator.Client; - -public interface IKubernetesClientFactory -{ - IImmutableDictionary RegisteredMetadata { get; } - - void RegisterMetadata(EntityMetadata metadata) - where TEntity : IKubernetesObject; - - GenericClient GetClient(IKubernetes? kubernetes = null) - where TEntity : IKubernetesObject; -} diff --git a/src/KubeOps.Operator/Client/KubernetesClientFactory.cs b/src/KubeOps.Operator/Client/KubernetesClientFactory.cs deleted file mode 100644 index d049436c..00000000 --- a/src/KubeOps.Operator/Client/KubernetesClientFactory.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Immutable; - -using k8s; -using k8s.Models; - -using KubeOps.Abstractions.Entities; - -namespace KubeOps.Operator.Client; - -internal class KubernetesClientFactory : IKubernetesClientFactory -{ - private readonly Dictionary _registeredMetadata = new(); - - public IImmutableDictionary RegisteredMetadata => _registeredMetadata.ToImmutableDictionary(); - - public void RegisterMetadata(EntityMetadata metadata) - where TEntity : IKubernetesObject - { - _registeredMetadata[typeof(TEntity)] = metadata; - } - - public GenericClient GetClient(IKubernetes? kubernetes = null) - where TEntity : IKubernetesObject - { - if (!_registeredMetadata.TryGetValue(typeof(TEntity), out var metadata)) - { - throw new InvalidOperationException( - $"No metadata registered for entity {typeof(TEntity).Name}. " + - "Please register metadata for this entity before using the client."); - } - - return metadata.Group switch - { - null => new GenericClient( - kubernetes ?? new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()), - metadata.Version, - metadata.PluralName), - _ => new GenericClient( - kubernetes ?? new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()), - metadata.Group, - metadata.Version, - metadata.PluralName), - }; - } -} diff --git a/src/KubeOps.Operator/KubeOps.Operator.csproj b/src/KubeOps.Operator/KubeOps.Operator.csproj index b64a8ffe..f099d2ea 100644 --- a/src/KubeOps.Operator/KubeOps.Operator.csproj +++ b/src/KubeOps.Operator/KubeOps.Operator.csproj @@ -22,6 +22,7 @@ + diff --git a/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs b/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs index bef1c028..fbc5ab7d 100644 --- a/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs +++ b/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs @@ -5,7 +5,7 @@ using k8s.Models; using KubeOps.Abstractions.Controller; -using KubeOps.Operator.Client; +using KubeOps.KubernetesClient; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -18,18 +18,18 @@ internal class ResourceWatcher : IHostedService { private readonly ILogger> _logger; private readonly IServiceProvider _provider; - private readonly GenericClient _client; + private readonly IKubernetesClient _client; private Watcher? _watcher; public ResourceWatcher( ILogger> logger, IServiceProvider provider, - IKubernetesClientFactory factory) + IKubernetesClient client) { _logger = logger; _provider = provider; - _client = factory.GetClient(); + _client = client; } public Task StartAsync(CancellationToken cancellationToken) @@ -61,7 +61,7 @@ private void WatchResource() } } - _watcher = _client.Watch(OnEvent, OnError, OnClosed); + _watcher = _client.Watch(OnEvent, OnError, OnClosed); } private void StopWatching() diff --git a/test/KubeOps.KubernetesClient.Test/GlobalUsings.cs b/test/KubeOps.KubernetesClient.Test/GlobalUsings.cs new file mode 100644 index 00000000..c802f448 --- /dev/null +++ b/test/KubeOps.KubernetesClient.Test/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/test/KubeOps.KubernetesClient.Test/KubeOps.KubernetesClient.Test.csproj b/test/KubeOps.KubernetesClient.Test/KubeOps.KubernetesClient.Test.csproj new file mode 100644 index 00000000..1104f6dd --- /dev/null +++ b/test/KubeOps.KubernetesClient.Test/KubeOps.KubernetesClient.Test.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs b/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs new file mode 100644 index 00000000..013db68b --- /dev/null +++ b/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs @@ -0,0 +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(); +}