Compare commits

...

10 Commits

Author SHA1 Message Date
dependabot[bot]
6b1da78877 Bump actions/upload-artifact from 4 to 5 (#1678)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
Docfx / docfx (push) Has been cancelled
Draft Release / draft (push) Has been cancelled
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 23:45:35 -07:00
Ivan Josipovic
70b6386441 feat(KubernetesJson): Use of SourceGenerationContext outside AOT (#1677)
* feat: initial source gen for json

* wip

* chore: readd default settings

* chore: cleanup

* chore: remove source gen from classic as it depends on C#9 support

* Update KubernetesClient.Classic.csproj

* wip

* enable UseStringEnumConverter

* chore: make converters public so we can use them in our libraries

* fix: recursion and remove converter from source gen

* fix: V1StatusObjectViewConverter

* wip

* wip

* wip

* fix: rfc3339 json serialization and yaml de/serialization

* chore: add namespace

* fix: imports

* fix: switch output to RFC3339Micro to fit Time and MicroTime

* chore: update AOT to match KubernetesYaml

* fix aot

* Update buildtest.yaml
2025-10-25 23:39:34 -07:00
Boshi Lian
292262755a revert change to structs from classes for IntOrString and ResourceQuantity, and handle null values in YAML converters (#1673)
* feat: add V2HorizontalPodAutoscaler integration test

* fix: change structs to classes for IntOrString and ResourceQuantity, and handle null values in YAML converters

* feat: implement equality members for ResourceQuantity class
2025-10-15 08:59:35 -07:00
Qing Long
6d27bd900b feat: enhance Kubernetes client with watch functionality (#1667)
* feat: enhance Kubernetes client with watch functionality

* refactor: simplify watch event handling in Kubernetes client example

* refactor: update Kubernetes watch functionality to use new event handling methods and add async enumerable support

* fix

* fix

* fix: correct usage of Pod list items in client example and update Obsolete attribute formatting

* fix: update client example to use correct Pod list method and improve Obsolete attribute formatting

* refactor: enhance type resolution for list items in TypeHelper by adding TryGetItemTypeFromSchema method

* feat: mark Watch methods as obsolete to prepare for future deprecation

* fix

* refactor: update WatcherExt class to internal and remove obsolete attributes; improve example method signature in Program.cs

* refactor: change WatcherExt class from internal to public and mark methods as obsolete for future deprecation
2025-10-11 15:10:53 -07:00
dependabot[bot]
ca5d9f4793 Bump github/codeql-action from 3 to 4 (#1669)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 22:07:01 -07:00
Boshi Lian
5de1c25cf1 migrate to record (#1665)
* migrate to record

* chore: update project files and clean up unused references

* refactor: convert classes to records and simplify constructors for IntOrString, ResourceQuantity, and V1Patch

* fix: define IsExternalInit to resolve CS0518 error in IntOrString

* refactor: change IntOrString and ResourceQuantity from records to structs, update implicit conversions, and simplify null checks

* refactor: add JsonPropertyName attribute to Value property in IntOrString struct

* refactor: simplify V1Patch constructor and improve argument validation

* refactor: remove unnecessary CultureInfo parameter in ToInt method

* Update src/KubernetesClient/Models/ResourceQuantity.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/KubernetesClient/Models/IntOrString.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Revert "Update src/KubernetesClient/Models/ResourceQuantity.cs"

This reverts commit 62b20a691554659e28d419067220dc1a0620133b.

* refactor: remove commented-out formatting check and simplify build command

* refactor: remove IValidate.cs from project references in Aot and Classic

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-22 14:20:13 -07:00
Boshi Lian
96955064cb generate base on 1.34 (#1654)
* Implement code changes to enhance functionality and improve performance

* chore: update version to 18.0 in version.json

* fix: correct initialization of opblackList in PluralHelper

* chore: update SDK version to 18.0 in README.md

* Fixdocfx (#82)

* fix: update file references and clean up validation comments in models

* chore: add symlink to CONTRIBUTING.md for easier access

* fix: update documentation to include full type names for WebSocket and Predicate

* fix: include CONTRIBUTING.md in docfx.json build content

* refactor: update IKubernetes interface and template for consistency

* feat: add Microsoft.CodeAnalysis.CSharp package and improve source normalization in generator context

* chore: update package versions in Directory.Packages.props for compatibility and improvements

* chore: update Microsoft.VisualStudio.SlnGen and Nerdbank.GitVersioning package versions for compatibility

* Implement code changes to enhance functionality and improve performance

* chore: update version to 18.0 in version.json

* fix: correct initialization of opblackList in PluralHelper

* chore: update SDK version to 18.0 in README.md

* refactor: update IKubernetes interface and template for consistency

* feat: add Microsoft.CodeAnalysis.CSharp package and improve source normalization in generator context

* chore: update package versions in Directory.Packages.props for compatibility and improvements

* chore: update Microsoft.VisualStudio.SlnGen and Nerdbank.GitVersioning package versions for compatibility

* chore: downgrade xunit.runner.visualstudio and Xunit.StaFact package versions for compatibility

* chore: update package versions in Directory.Packages.props for compatibility and improvements

* style: format code for consistency and readability

* feat: update certificate loading logic for .NET 9 compatibility

* fix: update certificate loading method for .NET 9 compatibility
2025-09-22 14:18:16 -07:00
Boshi Lian
11a9641fbe refactor: reorganize Kubernetes client models into separate files (#1663) 2025-09-16 07:40:17 -07:00
dependabot[bot]
a99fbe4693 Bump actions/setup-dotnet from 4 to 5 (#1658)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4 to 5.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-05 12:25:27 -07:00
Ivan Josipovic
885b9ae09d fix(yaml): byte[] serialization (#1657)
* fix(yaml): byte[] serialization

* fix: tests and code

* chore: add null check

* chore: add configmap tests
2025-09-05 12:17:25 -07:00
77 changed files with 8923 additions and 5279 deletions

View File

@@ -12,17 +12,13 @@ jobs:
with:
fetch-depth: 0
- name: Setup dotnet
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
9.0.x
# - name: Check Format
# # don't check formatting on Windows b/c of CRLF issues.
# if: matrix.os == 'ubuntu-latest'
# run: dotnet format --severity error --verify-no-changes --exclude ./src/KubernetesClient/generated/
- name: Build
run: dotnet build --configuration Release -v detailed
run: dotnet build --configuration Release
- name: Test
run: dotnet test --configuration Release --collect:"Code Coverage;Format=Cobertura" --logger trx --results-directory TestResults --settings CodeCoverage.runsettings --no-build
- name: Upload coverage to Codecov
@@ -31,7 +27,7 @@ jobs:
directory: ./TestResults
files: '*.cobertura.xml'
- name: Upload test results
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: test-results-${{ matrix.os }}
path: ./TestResults
@@ -48,7 +44,7 @@ jobs:
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v2
- name: Setup dotnet SDK
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: '9.0.x'
- name: Restore nugets (msbuild)
@@ -63,7 +59,7 @@ jobs:
with:
fetch-depth: 0
- name: Setup dotnet
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x

View File

@@ -31,7 +31,7 @@ jobs:
fetch-depth: 0
- name: Setup dotnet
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
@@ -39,7 +39,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -59,4 +59,4 @@ jobs:
run: dotnet build --configuration Debug --no-restore
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4

View File

@@ -30,7 +30,7 @@ jobs:
fetch-depth: 0
- name: Setup dotnet
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x

View File

@@ -18,7 +18,7 @@ jobs:
fetch-depth: 0
- name: Setup dotnet
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x

View File

@@ -15,7 +15,7 @@ jobs:
fetch-depth: 0
- name: Setup dotnet
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x

View File

@@ -3,51 +3,52 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.5.1" />
<PackageVersion Include="FluentAssertions" Version="8.2.0" />
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.6.2" />
<PackageVersion Include="FluentAssertions" Version="8.6.0" />
<PackageVersion Include="Fractions" Version="7.3.0" />
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
<PackageVersion Include="MartinCostello.Logging.XUnit" Version="0.5.1" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageVersion Include="Microsoft.TestPlatform.ObjectModel" Version="17.13.0" />
<PackageVersion Include="MartinCostello.Logging.XUnit" Version="0.6.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.8" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Microsoft.TestPlatform.ObjectModel" Version="17.14.1" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
<PackageVersion Include="Nito.AsyncEx.Coordination" Version="5.1.2" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.11.2" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.11.1" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageVersion Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.4" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.14" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="System.Text.Json" Version="9.0.4" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="9.0.4" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.8" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.15" />
<PackageVersion Include="System.Reactive" Version="6.0.2" />
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
<PackageVersion Include="Vecc.YamlDotNet.Analyzers.StaticGenerator" Version="16.3.0" />
<PackageVersion Include="Wiremock.Net" Version="1.7.4" />
<PackageVersion Include="Wiremock.Net" Version="1.12.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2" />
<PackageVersion Include="Xunit.StaFact" Version="1.2.69" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Autofac" Version="8.2.1" />
<PackageVersion Include="Autofac" Version="8.4.0" />
<PackageVersion Include="CaseExtensions" Version="1.1.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.13.0" />
<PackageVersion Include="Namotion.Reflection" Version="3.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Namotion.Reflection" Version="3.4.2" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NJsonSchema" Version="11.2.0" />
<PackageVersion Include="NJsonSchema.Annotations" Version="11.2.0" />
<PackageVersion Include="NSwag.Core" Version="14.3.0" />
<PackageVersion Include="NJsonSchema" Version="11.4.0" />
<PackageVersion Include="NJsonSchema.Annotations" Version="11.4.0" />
<PackageVersion Include="NSwag.Core" Version="14.5.0" />
<PackageVersion Include="Scriban" Version="6.2.1" />
</ItemGroup>
<ItemGroup>
<GlobalPackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<GlobalPackageReference Include="Microsoft.VisualStudio.SlnGen" Version="12.0.3" />
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.7.112" />
<GlobalPackageReference Include="Microsoft.VisualStudio.SlnGen" Version="12.0.23" />
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.7.115" />
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
</ItemGroup>
</Project>
</Project>

View File

@@ -154,6 +154,7 @@ ${GEN_DIR}/openapi/csharp.sh ${REPO_DIR}/src/KubernetesClient ${REPO_DIR}/csharp
| SDK Version | Kubernetes Version | .NET Targeting |
|-------------|--------------------|-----------------------------------------------------|
| 18.0 | 1.34 | net8.0;net9.0;net48*;netstandard2.0* |
| 17.0 | 1.33 | net8.0;net9.0;net48*;netstandard2.0* |
| 16.0 | 1.32 | net8.0;net9.0;net48*;netstandard2.0* |
| 15.0 | 1.31 | net6.0;net8.0;net48*;netstandard2.0* |

View File

@@ -1,3 +1,3 @@
export KUBERNETES_BRANCH=v1.33.0
export KUBERNETES_BRANCH=v1.34.0
export CLIENT_VERSION=0.0.1
export PACKAGE_NAME=k8s

View File

@@ -1,5 +1,5 @@
// See https://aka.ms/new-console-template for more information
using k8s;
using k8s;
using k8s.Models;
using k8s.ClientSets;
using System.Threading.Tasks;
@@ -12,15 +12,21 @@ namespace clientset
var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
var client = new Kubernetes(config);
ClientSet clientSet = new ClientSet(client);
var clientSet = new ClientSet(client);
var list = await clientSet.CoreV1.Pod.ListAsync("default").ConfigureAwait(false);
foreach (var item in list)
{
System.Console.WriteLine(item.Metadata.Name);
}
var pod = await clientSet.CoreV1.Pod.GetAsync("test","default").ConfigureAwait(false);
var pod = await clientSet.CoreV1.Pod.GetAsync("test", "default").ConfigureAwait(false);
System.Console.WriteLine(pod?.Metadata?.Name);
var watch = clientSet.CoreV1.Pod.WatchListAsync("default");
await foreach (var (_, item) in watch.ConfigureAwait(false))
{
System.Console.WriteLine(item.Metadata.Name);
}
}
}
}
}

View File

@@ -67,7 +67,15 @@ var old = JsonSerializer.SerializeToDocument(readCert, serializeOptions);
var replace = new List<V1CertificateSigningRequestCondition>
{
new ("True", "Approved", DateTime.UtcNow, DateTime.UtcNow, "This certificate was approved by k8s client", "Approve"),
new V1CertificateSigningRequestCondition
{
Type = "Approved",
Status = "True",
Reason = "Approve",
Message = "This certificate was approved by k8s client",
LastUpdateTime = DateTime.UtcNow,
LastTransitionTime = DateTime.UtcNow,
},
};
readCert.Status.Conditions = replace;

View File

@@ -19,13 +19,13 @@ namespace customResource
}
}
public class CResourceSpec
public record CResourceSpec
{
[JsonPropertyName("cityName")]
public string CityName { get; set; }
}
public class CResourceStatus : V1Status
public record CResourceStatus : V1Status
{
[JsonPropertyName("temperature")]
public string Temperature { get; set; }

View File

@@ -23,7 +23,7 @@ var pod = new V1Pod
{
Requests = new Dictionary<string, ResourceQuantity>()
{
["cpu"] = new ResourceQuantity("100m"),
["cpu"] = "100m",
},
},
},

View File

@@ -1,5 +1,4 @@
using k8s;
using k8s.Models;
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -8,9 +7,10 @@ var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
IKubernetes client = new Kubernetes(config);
var podlistResp = client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true);
var podlistResp = client.CoreV1.WatchListNamespacedPodAsync("default");
// C# 8 required https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8
await foreach (var (type, item) in podlistResp.WatchAsync<V1Pod, V1PodList>().ConfigureAwait(false))
await foreach (var (type, item) in podlistResp.ConfigureAwait(false))
{
Console.WriteLine("==on watch event==");
Console.WriteLine(type);
@@ -22,8 +22,7 @@ await foreach (var (type, item) in podlistResp.WatchAsync<V1Pod, V1PodList>().Co
void WatchUsingCallback(IKubernetes client)
#pragma warning restore CS8321 // Remove unused private members
{
var podlistResp = client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true);
using (podlistResp.Watch<V1Pod, V1PodList>((type, item) =>
using (var podlistResp = client.CoreV1.WatchListNamespacedPod("default", onEvent: (type, item) =>
{
Console.WriteLine("==on watch event==");
Console.WriteLine(type);
@@ -37,4 +36,4 @@ void WatchUsingCallback(IKubernetes client)
Console.CancelKeyPress += (sender, eventArgs) => ctrlc.Set();
ctrlc.Wait();
}
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
@@ -20,20 +20,18 @@
<Compile Include="..\KubernetesClient\Extensions.cs" />
<Compile Include="..\KubernetesClient\FloatEmitter.cs" />
<Compile Include="..\KubernetesClient\Models\GeneratedModelVersion.cs" />
<Compile Include="..\KubernetesClient\IItems.cs" />
<Compile Include="..\KubernetesClient\Models\IItems.cs" />
<Compile Include="..\KubernetesClient\IKubernetesObject.cs" />
<Compile Include="..\KubernetesClient\IMetadata.cs" />
<Compile Include="..\KubernetesClient\Models\IMetadata.cs" />
<Compile Include="..\KubernetesClient\Models\IntOrStringJsonConverter.cs" />
<Compile Include="..\KubernetesClient\Models\IntOrStringYamlConverter.cs" />
<Compile Include="..\KubernetesClient\Models\IntstrIntOrString.cs" />
<Compile Include="..\KubernetesClient\ISpec.cs" />
<Compile Include="..\KubernetesClient\IStatus.cs" />
<Compile Include="..\KubernetesClient\IValidate.cs" />
<Compile Include="..\KubernetesClient\Models\IntOrString.cs" />
<Compile Include="..\KubernetesClient\Models\ISpec.cs" />
<Compile Include="..\KubernetesClient\Models\IStatus.cs" />
<Compile Include="..\KubernetesClient\Models\KubernetesEntityAttribute.cs" />
<Compile Include="..\KubernetesClient\Models\KubernetesList.cs" />
<Compile Include="..\KubernetesClient\KubernetesObject.cs" />
<Compile Include="..\KubernetesClient\Models\ModelExtensions.cs" />
<Compile Include="..\KubernetesClient\Models\ModelVersionConverter.cs" />
<Compile Include="..\KubernetesClient\Models\NodeMetrics.cs" />
<Compile Include="..\KubernetesClient\Models\NodeMetricsList.cs" />
<Compile Include="..\KubernetesClient\Models\PodMetrics.cs" />
@@ -45,7 +43,11 @@
<Compile Include="..\KubernetesClient\Models\V1Patch.cs" />
<Compile Include="..\KubernetesClient\Models\V1PodTemplateSpec.cs" />
<Compile Include="..\KubernetesClient\Models\V1Status.cs" />
<Compile Include="..\KubernetesClient\KubernetesJson.cs" />
<Compile Include="..\KubernetesClient\SourceGenerationContext.cs" />
<Compile Include="..\KubernetesClient\Models\V1Status.ObjectView.cs" />
<Compile Include="..\KubernetesClient\Models\KubernetesDateTimeOffsetYamlConverter.cs" />
<Compile Include="..\KubernetesClient\Models\KubernetesDateTimeYamlConverter.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\KubernetesClient\ClientSets\ClientSet.cs" />

View File

@@ -1,103 +0,0 @@
using System.Globalization;
using System.Text.RegularExpressions;
using System.Xml;
namespace k8s
{
internal static class KubernetesJson
{
internal sealed class Iso8601TimeSpanConverter : JsonConverter<TimeSpan>
{
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
return XmlConvert.ToTimeSpan(str);
}
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
{
var iso8601TimeSpanString = XmlConvert.ToString(value); // XmlConvert for TimeSpan uses ISO8601, so delegate serialization to it
writer.WriteStringValue(iso8601TimeSpanString);
}
}
internal sealed class KubernetesDateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
private const string RFC3339MicroFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.ffffffK";
private const string RFC3339NanoFormat = "yyyy-MM-dd'T'HH':'mm':'ss.fffffffK";
private const string RFC3339Format = "yyyy'-'MM'-'dd'T'HH':'mm':'ssK";
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
if (DateTimeOffset.TryParseExact(str, new[] { RFC3339Format, RFC3339MicroFormat }, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result))
{
return result;
}
// try RFC3339NanoLenient by trimming 1-9 digits to 7 digits
var originalstr = str;
str = Regex.Replace(str, @"\.\d+", m => (m.Value + "000000000").Substring(0, 7 + 1)); // 7 digits + 1 for the dot
if (DateTimeOffset.TryParseExact(str, new[] { RFC3339NanoFormat }, CultureInfo.InvariantCulture, DateTimeStyles.None, out result))
{
return result;
}
throw new FormatException($"Unable to parse {originalstr} as RFC3339 RFC3339Micro or RFC3339Nano");
}
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(RFC3339MicroFormat));
}
}
internal sealed class KubernetesDateTimeConverter : JsonConverter<DateTime>
{
private static readonly JsonConverter<DateTimeOffset> UtcConverter = new KubernetesDateTimeOffsetConverter();
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return UtcConverter.Read(ref reader, typeToConvert, options).UtcDateTime;
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
UtcConverter.Write(writer, value, options);
}
}
/// <summary>
/// Configures <see cref="JsonSerializerOptions"/> for the <see cref="JsonSerializer"/>.
/// To override existing converters, add them to the top of the <see cref="JsonSerializerOptions.Converters"/> list
/// e.g. as follows: <code>options.Converters.Insert(index: 0, new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));</code>
/// </summary>
/// <param name="configure">An <see cref="Action"/> to configure the <see cref="JsonSerializerOptions"/>.</param>
public static void AddJsonOptions(Action<JsonSerializerOptions> configure)
{
}
public static TValue Deserialize<TValue>(string json, JsonSerializerOptions jsonSerializerOptions = null)
{
var info = SourceGenerationContext.Default.GetTypeInfo(typeof(TValue));
return (TValue)JsonSerializer.Deserialize(json, info);
}
public static TValue Deserialize<TValue>(Stream json, JsonSerializerOptions jsonSerializerOptions = null)
{
var info = SourceGenerationContext.Default.GetTypeInfo(typeof(TValue));
return (TValue)JsonSerializer.Deserialize(json, info);
}
public static string Serialize(object value, JsonSerializerOptions jsonSerializerOptions = null)
{
if (value is V1Patch { Content: string jsonValue })
{
return jsonValue;
}
var info = SourceGenerationContext.Default.GetTypeInfo(value.GetType());
return JsonSerializer.Serialize(value, info);
}
}
}

View File

@@ -17,6 +17,8 @@ namespace k8s
.WithTypeConverter(new IntOrStringYamlConverter())
.WithTypeConverter(new ByteArrayStringYamlConverter())
.WithTypeConverter(new ResourceQuantityYamlConverter())
.WithTypeConverter(new KubernetesDateTimeYamlConverter())
.WithTypeConverter(new KubernetesDateTimeOffsetYamlConverter())
.WithAttemptingUnquotedStringTypeDeserialization()
;
@@ -33,6 +35,8 @@ namespace k8s
.WithTypeConverter(new IntOrStringYamlConverter())
.WithTypeConverter(new ByteArrayStringYamlConverter())
.WithTypeConverter(new ResourceQuantityYamlConverter())
.WithTypeConverter(new KubernetesDateTimeYamlConverter())
.WithTypeConverter(new KubernetesDateTimeOffsetYamlConverter())
.WithEventEmitter(e => new StringQuotingEmitter(e))
.WithEventEmitter(e => new FloatEmitter(e))
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull)
@@ -56,7 +60,7 @@ namespace k8s
return null;
}
return Encoding.UTF8.GetBytes(scalar.Value);
return Convert.FromBase64String(scalar.Value);
}
finally
{
@@ -69,8 +73,15 @@ namespace k8s
public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer)
{
if (value == null)
{
emitter.Emit(new Scalar(string.Empty));
return;
}
var obj = (byte[])value;
emitter?.Emit(new Scalar(Encoding.UTF8.GetString(obj)));
var encoded = Convert.ToBase64String(obj);
emitter.Emit(new Scalar(encoded));
}
}

View File

@@ -1,12 +0,0 @@
using static k8s.KubernetesJson;
namespace k8s;
[JsonSourceGenerationOptions(
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
Converters = new[] { typeof(Iso8601TimeSpanConverter), typeof(KubernetesDateTimeConverter), typeof(KubernetesDateTimeOffsetConverter) })
]
internal partial class SourceGenerationContext : JsonSerializerContext
{
}

View File

@@ -0,0 +1,5 @@
// IntOrString.cs(7,36): error CS0518: Predefined type 'System.Runtime.CompilerServices.IsExternalInit' is not defined or imported
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit { }
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net48</TargetFrameworks>
@@ -22,22 +22,20 @@
<Compile Include="..\KubernetesClient\Extensions.cs" />
<Compile Include="..\KubernetesClient\FloatEmitter.cs" />
<Compile Include="..\KubernetesClient\Models\GeneratedModelVersion.cs" />
<Compile Include="..\KubernetesClient\IItems.cs" />
<Compile Include="..\KubernetesClient\Models\IItems.cs" />
<Compile Include="..\KubernetesClient\IKubernetesObject.cs" />
<Compile Include="..\KubernetesClient\IMetadata.cs" />
<Compile Include="..\KubernetesClient\Models\IMetadata.cs" />
<Compile Include="..\KubernetesClient\Models\IntOrStringJsonConverter.cs" />
<Compile Include="..\KubernetesClient\Models\IntOrStringYamlConverter.cs" />
<Compile Include="..\KubernetesClient\Models\IntstrIntOrString.cs" />
<Compile Include="..\KubernetesClient\ISpec.cs" />
<Compile Include="..\KubernetesClient\IStatus.cs" />
<Compile Include="..\KubernetesClient\IValidate.cs" />
<Compile Include="..\KubernetesClient\Models\IntOrString.cs" />
<Compile Include="..\KubernetesClient\Models\ISpec.cs" />
<Compile Include="..\KubernetesClient\Models\IStatus.cs" />
<Compile Include="..\KubernetesClient\Models\KubernetesEntityAttribute.cs" />
<Compile Include="..\KubernetesClient\KubernetesJson.cs" />
<Compile Include="..\KubernetesClient\Models\KubernetesList.cs" />
<Compile Include="..\KubernetesClient\KubernetesObject.cs" />
<Compile Include="..\KubernetesClient\KubernetesYaml.cs" />
<Compile Include="..\KubernetesClient\Models\ModelExtensions.cs" />
<Compile Include="..\KubernetesClient\Models\ModelVersionConverter.cs" />
<Compile Include="..\KubernetesClient\Models\NodeMetrics.cs" />
<Compile Include="..\KubernetesClient\Models\NodeMetricsList.cs" />
<Compile Include="..\KubernetesClient\Models\PodMetrics.cs" />
@@ -51,6 +49,9 @@
<Compile Include="..\KubernetesClient\Models\V1PodTemplateSpec.cs" />
<Compile Include="..\KubernetesClient\Models\V1Status.cs" />
<Compile Include="..\KubernetesClient\Models\V1Status.ObjectView.cs" />
<Compile Include="..\KubernetesClient\Models\KubernetesDateTimeOffsetYamlConverter.cs" />
<Compile Include="..\KubernetesClient\Models\KubernetesDateTimeYamlConverter.cs" />
<Compile Include="..\KubernetesClient\KubeConfigModels\ClusterEndpoint.cs" />
<Compile Include="..\KubernetesClient\KubeConfigModels\Context.cs" />

View File

@@ -1,28 +0,0 @@
namespace k8s
{
/// <summary>
/// Kubernetes object that exposes list of objects
/// </summary>
/// <typeparam name="T">type of the objects</typeparam>
public interface IItems<T>
{
/// <summary>
/// Gets or sets list of objects. More info:
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md
/// </summary>
IList<T> Items { get; set; }
}
public static class ItemsExt
{
public static IEnumerator<T> GetEnumerator<T>(this IItems<T> items)
{
if (items is null)
{
throw new ArgumentNullException(nameof(items));
}
return items.Items.GetEnumerator();
}
}
}

View File

@@ -1,6 +1,6 @@
namespace k8s;
public partial interface IKubernetes : IBasicKubernetes, IDisposable
public partial interface IKubernetes : IDisposable
{
/// <summary>
/// The base URI of the service.

View File

@@ -1,16 +0,0 @@
namespace k8s
{
/// <summary>
/// Kubernetes object that exposes metadata
/// </summary>
/// <typeparam name="T">Type of metadata exposed. Usually this will be either
/// <see cref="V1ListMeta"/> for lists or <see cref="V1ObjectMeta"/> for objects</typeparam>
public interface IMetadata<T>
{
/// <summary>
/// Gets or sets standard object's metadata. More info:
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
/// </summary>
T Metadata { get; set; }
}
}

View File

@@ -1,16 +0,0 @@
namespace k8s
{
/// <summary>
/// Represents a Kubernetes object that has a spec
/// </summary>
/// <typeparam name="T">type of Kubernetes object</typeparam>
public interface ISpec<T>
{
/// <summary>
/// Gets or sets specification of the desired behavior of the entity. More
/// info:
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
/// </summary>
T Spec { get; set; }
}
}

View File

@@ -1,13 +0,0 @@
namespace k8s
{
/// <summary>
/// Object that allows self validation
/// </summary>
public interface IValidate
{
/// <summary>
/// Validate the object.
/// </summary>
void Validate();
}
}

View File

@@ -3,13 +3,17 @@ using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Xml;
#if NET8_0_OR_GREATER
using System.Text.Json.Serialization.Metadata;
#endif
namespace k8s
{
public static class KubernetesJson
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions();
internal static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions();
private sealed class Iso8601TimeSpanConverter : JsonConverter<TimeSpan>
public sealed class Iso8601TimeSpanConverter : JsonConverter<TimeSpan>
{
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
@@ -24,11 +28,11 @@ namespace k8s
}
}
private sealed class KubernetesDateTimeOffsetConverter : JsonConverter<DateTimeOffset>
public sealed class KubernetesDateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
private const string RFC3339MicroFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.ffffffK";
private const string RFC3339NanoFormat = "yyyy-MM-dd'T'HH':'mm':'ss.fffffffK";
private const string RFC3339Format = "yyyy'-'MM'-'dd'T'HH':'mm':'ssK";
private const string RFC3339MicroFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.ffffffZ";
private const string RFC3339NanoFormat = "yyyy-MM-dd'T'HH':'mm':'ss.fffffffZ";
private const string RFC3339Format = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ";
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
@@ -50,13 +54,22 @@ namespace k8s
throw new FormatException($"Unable to parse {originalstr} as RFC3339 RFC3339Micro or RFC3339Nano");
}
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(RFC3339MicroFormat));
// Output as RFC3339Micro
var date = value.ToUniversalTime();
var basePart = date.ToString("yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture);
var frac = date.ToString(".ffffff", CultureInfo.InvariantCulture)
.TrimEnd('0')
.TrimEnd('.');
writer.WriteStringValue(basePart + frac + "Z");
}
}
private sealed class KubernetesDateTimeConverter : JsonConverter<DateTime>
public sealed class KubernetesDateTimeConverter : JsonConverter<DateTime>
{
private static readonly JsonConverter<DateTimeOffset> UtcConverter = new KubernetesDateTimeOffsetConverter();
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
@@ -72,13 +85,22 @@ namespace k8s
static KubernetesJson()
{
#if K8S_AOT
// Uses Source Generated IJsonTypeInfoResolver
JsonSerializerOptions.TypeInfoResolver = SourceGenerationContext.Default;
#else
#if NET8_0_OR_GREATER
// Uses Source Generated IJsonTypeInfoResolver when available and falls back to reflection
JsonSerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(SourceGenerationContext.Default, new DefaultJsonTypeInfoResolver());
#endif
JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
#endif
JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
JsonSerializerOptions.Converters.Add(new Iso8601TimeSpanConverter());
JsonSerializerOptions.Converters.Add(new KubernetesDateTimeConverter());
JsonSerializerOptions.Converters.Add(new KubernetesDateTimeOffsetConverter());
JsonSerializerOptions.Converters.Add(new V1Status.V1StatusObjectViewConverter());
JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
}
/// <summary>
@@ -99,47 +121,92 @@ namespace k8s
public static TValue Deserialize<TValue>(string json, JsonSerializerOptions jsonSerializerOptions = null)
{
#if NET8_0_OR_GREATER
var info = (JsonTypeInfo<TValue>)(jsonSerializerOptions ?? JsonSerializerOptions).GetTypeInfo(typeof(TValue));
return JsonSerializer.Deserialize(json, info);
#else
return JsonSerializer.Deserialize<TValue>(json, jsonSerializerOptions ?? JsonSerializerOptions);
#endif
}
public static TValue Deserialize<TValue>(Stream json, JsonSerializerOptions jsonSerializerOptions = null)
{
#if NET8_0_OR_GREATER
var info = (JsonTypeInfo<TValue>)(jsonSerializerOptions ?? JsonSerializerOptions).GetTypeInfo(typeof(TValue));
return JsonSerializer.Deserialize(json, info);
#else
return JsonSerializer.Deserialize<TValue>(json, jsonSerializerOptions ?? JsonSerializerOptions);
#endif
}
public static TValue Deserialize<TValue>(JsonDocument json, JsonSerializerOptions jsonSerializerOptions = null)
{
#if NET8_0_OR_GREATER
var info = (JsonTypeInfo<TValue>)(jsonSerializerOptions ?? JsonSerializerOptions).GetTypeInfo(typeof(TValue));
return JsonSerializer.Deserialize(json, info);
#else
return JsonSerializer.Deserialize<TValue>(json, jsonSerializerOptions ?? JsonSerializerOptions);
#endif
}
public static TValue Deserialize<TValue>(JsonElement json, JsonSerializerOptions jsonSerializerOptions = null)
{
#if NET8_0_OR_GREATER
var info = (JsonTypeInfo<TValue>)(jsonSerializerOptions ?? JsonSerializerOptions).GetTypeInfo(typeof(TValue));
return JsonSerializer.Deserialize(json, info);
#else
return JsonSerializer.Deserialize<TValue>(json, jsonSerializerOptions ?? JsonSerializerOptions);
#endif
}
public static TValue Deserialize<TValue>(JsonNode json, JsonSerializerOptions jsonSerializerOptions = null)
{
#if NET8_0_OR_GREATER
var info = (JsonTypeInfo<TValue>)(jsonSerializerOptions ?? JsonSerializerOptions).GetTypeInfo(typeof(TValue));
return JsonSerializer.Deserialize(json, info);
#else
return JsonSerializer.Deserialize<TValue>(json, jsonSerializerOptions ?? JsonSerializerOptions);
#endif
}
public static string Serialize(object value, JsonSerializerOptions jsonSerializerOptions = null)
{
#if NET8_0_OR_GREATER
var info = (jsonSerializerOptions ?? JsonSerializerOptions).GetTypeInfo(value.GetType());
return JsonSerializer.Serialize(value, info);
#else
return JsonSerializer.Serialize(value, jsonSerializerOptions ?? JsonSerializerOptions);
#endif
}
public static string Serialize(JsonDocument value, JsonSerializerOptions jsonSerializerOptions = null)
{
#if NET8_0_OR_GREATER
var info = (jsonSerializerOptions ?? JsonSerializerOptions).GetTypeInfo(value.GetType());
return JsonSerializer.Serialize(value, info);
#else
return JsonSerializer.Serialize(value, jsonSerializerOptions ?? JsonSerializerOptions);
#endif
}
public static string Serialize(JsonElement value, JsonSerializerOptions jsonSerializerOptions = null)
{
#if NET8_0_OR_GREATER
var info = (jsonSerializerOptions ?? JsonSerializerOptions).GetTypeInfo(value.GetType());
return JsonSerializer.Serialize(value, info);
#else
return JsonSerializer.Serialize(value, jsonSerializerOptions ?? JsonSerializerOptions);
#endif
}
public static string Serialize(JsonNode value, JsonSerializerOptions jsonSerializerOptions = null)
{
#if NET8_0_OR_GREATER
var info = (jsonSerializerOptions ?? JsonSerializerOptions).GetTypeInfo(value.GetType());
return JsonSerializer.Serialize(value, info);
#else
return JsonSerializer.Serialize(value, jsonSerializerOptions ?? JsonSerializerOptions);
#endif
}
}
}

View File

@@ -21,6 +21,8 @@ namespace k8s
.WithTypeConverter(new IntOrStringYamlConverter())
.WithTypeConverter(new ByteArrayStringYamlConverter())
.WithTypeConverter(new ResourceQuantityYamlConverter())
.WithTypeConverter(new KubernetesDateTimeYamlConverter())
.WithTypeConverter(new KubernetesDateTimeOffsetYamlConverter())
.WithAttemptingUnquotedStringTypeDeserialization()
.WithOverridesFromJsonPropertyAttributes();
@@ -41,6 +43,8 @@ namespace k8s
.WithTypeConverter(new IntOrStringYamlConverter())
.WithTypeConverter(new ByteArrayStringYamlConverter())
.WithTypeConverter(new ResourceQuantityYamlConverter())
.WithTypeConverter(new KubernetesDateTimeYamlConverter())
.WithTypeConverter(new KubernetesDateTimeOffsetYamlConverter())
.WithEventEmitter(e => new StringQuotingEmitter(e))
.WithEventEmitter(e => new FloatEmitter(e))
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull)
@@ -78,7 +82,7 @@ namespace k8s
return null;
}
return Encoding.UTF8.GetBytes(scalar.Value);
return Convert.FromBase64String(scalar.Value);
}
finally
{
@@ -91,19 +95,15 @@ namespace k8s
public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer)
{
if (value == null)
{
emitter.Emit(new Scalar(string.Empty));
return;
}
var obj = (byte[])value;
var strValue = Encoding.UTF8.GetString(obj);
// Check if the string is multi-line by looking for a newline character.
var scalarStyle = strValue.Contains('\n') ? ScalarStyle.Literal : ScalarStyle.Any;
emitter.Emit(new Scalar(
AnchorName.Empty,
TagName.Empty,
strValue,
scalarStyle,
true,
true));
var encoded = Convert.ToBase64String(obj);
emitter.Emit(new Scalar(encoded));
}
}

View File

@@ -0,0 +1,27 @@
namespace k8s.Models;
/// <summary>
/// Kubernetes object that exposes list of objects
/// </summary>
/// <typeparam name="T">type of the objects</typeparam>
public interface IItems<T>
{
/// <summary>
/// Gets or sets list of objects. More info:
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md
/// </summary>
IList<T> Items { get; set; }
}
public static class ItemsExt
{
public static IEnumerator<T> GetEnumerator<T>(this IItems<T> items)
{
if (items is null)
{
throw new ArgumentNullException(nameof(items));
}
return items.Items.GetEnumerator();
}
}

View File

@@ -0,0 +1,15 @@
namespace k8s.Models;
/// <summary>
/// Kubernetes object that exposes metadata
/// </summary>
/// <typeparam name="T">Type of metadata exposed. Usually this will be either
/// <see cref="V1ListMeta"/> for lists or <see cref="V1ObjectMeta"/> for objects</typeparam>
public interface IMetadata<T>
{
/// <summary>
/// Gets or sets standard object's metadata. More info:
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
/// </summary>
T Metadata { get; set; }
}

View File

@@ -0,0 +1,15 @@
namespace k8s.Models;
/// <summary>
/// Represents a Kubernetes object that has a spec
/// </summary>
/// <typeparam name="T">type of Kubernetes object</typeparam>
public interface ISpec<T>
{
/// <summary>
/// Gets or sets specification of the desired behavior of the entity. More
/// info:
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
/// </summary>
T Spec { get; set; }
}

View File

@@ -1,4 +1,4 @@
namespace k8s
namespace k8s.Models
{
/// <summary>
/// Kubernetes object that exposes status

View File

@@ -0,0 +1,38 @@
namespace k8s.Models
{
[JsonConverter(typeof(IntOrStringJsonConverter))]
public class IntOrString
{
public string Value { get; private init; }
public static implicit operator IntOrString(int v)
{
return Convert.ToString(v);
}
public static implicit operator IntOrString(long v)
{
return Convert.ToString(v);
}
public static implicit operator string(IntOrString v)
{
return v?.Value;
}
public static implicit operator IntOrString(string v)
{
return new IntOrString { Value = v };
}
public override string ToString()
{
return Value;
}
public int ToInt()
{
return int.Parse(Value);
}
}
}

View File

@@ -1,8 +1,8 @@
namespace k8s.Models
{
internal sealed class IntOrStringJsonConverter : JsonConverter<IntstrIntOrString>
internal sealed class IntOrStringJsonConverter : JsonConverter<IntOrString>
{
public override IntstrIntOrString Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override IntOrString Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
@@ -17,14 +17,14 @@ namespace k8s.Models
throw new NotSupportedException();
}
public override void Write(Utf8JsonWriter writer, IntstrIntOrString value, JsonSerializerOptions options)
public override void Write(Utf8JsonWriter writer, IntOrString value, JsonSerializerOptions options)
{
if (writer == null)
{
throw new ArgumentNullException(nameof(writer));
}
var s = value?.Value;
var s = value.Value;
if (long.TryParse(s, out var intv))
{

View File

@@ -7,7 +7,7 @@ namespace k8s.Models
{
public bool Accepts(Type type)
{
return type == typeof(IntstrIntOrString);
return type == typeof(IntOrString);
}
public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
@@ -21,7 +21,7 @@ namespace k8s.Models
return null;
}
return new IntstrIntOrString(scalar?.Value);
return scalar?.Value;
}
finally
{
@@ -34,7 +34,7 @@ namespace k8s.Models
public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer)
{
var obj = (IntstrIntOrString)value;
var obj = (IntOrString)value;
emitter?.Emit(new YamlDotNet.Core.Events.Scalar(obj?.Value));
}
}

View File

@@ -1,56 +0,0 @@
namespace k8s.Models
{
[JsonConverter(typeof(IntOrStringJsonConverter))]
public partial class IntstrIntOrString
{
public static implicit operator IntstrIntOrString(int v)
{
return new IntstrIntOrString(Convert.ToString(v));
}
public static implicit operator IntstrIntOrString(long v)
{
return new IntstrIntOrString(Convert.ToString(v));
}
public static implicit operator string(IntstrIntOrString v)
{
return v?.Value;
}
public static implicit operator IntstrIntOrString(string v)
{
return new IntstrIntOrString(v);
}
protected bool Equals(IntstrIntOrString other)
{
return string.Equals(Value, other?.Value);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != GetType())
{
return false;
}
return Equals((IntstrIntOrString)obj);
}
public override int GetHashCode()
{
return Value != null ? Value.GetHashCode() : 0;
}
}
}

View File

@@ -0,0 +1,64 @@
using System.Globalization;
using System.Text.RegularExpressions;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
namespace k8s.Models;
public sealed class KubernetesDateTimeOffsetYamlConverter : IYamlTypeConverter
{
private const string RFC3339MicroFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.ffffff'Z'";
private const string RFC3339NanoFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffff'Z'";
private const string RFC3339Format = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'";
public bool Accepts(Type type) => type == typeof(DateTimeOffset);
public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{
if (parser?.Current is Scalar scalar)
{
try
{
if (string.IsNullOrEmpty(scalar.Value))
{
return null;
}
var str = scalar.Value;
if (DateTimeOffset.TryParseExact(str, new[] { RFC3339Format, RFC3339MicroFormat }, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result))
{
return result;
}
// try RFC3339NanoLenient by trimming 1-9 digits to 7 digits
var originalstr = str;
str = Regex.Replace(str, @"\.\d+", m => (m.Value + "000000000").Substring(0, 7 + 1)); // 7 digits + 1 for the dot
if (DateTimeOffset.TryParseExact(str, new[] { RFC3339NanoFormat }, CultureInfo.InvariantCulture, DateTimeStyles.None, out result))
{
return result;
}
}
finally
{
parser.MoveNext();
}
}
throw new InvalidOperationException($"Unable to parse '{parser.Current?.ToString()}' as RFC3339, RFC3339Micro, or RFC3339Nano");
}
public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer)
{
// Output as RFC3339Micro
var date = ((DateTimeOffset)value).ToUniversalTime();
var basePart = date.ToString("yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture);
var frac = date.ToString(".ffffff", CultureInfo.InvariantCulture)
.TrimEnd('0')
.TrimEnd('.');
emitter.Emit(new Scalar(AnchorName.Empty, TagName.Empty, basePart + frac + "Z", ScalarStyle.DoubleQuoted, true, false));
}
}

View File

@@ -0,0 +1,23 @@
using YamlDotNet.Core;
using YamlDotNet.Serialization;
namespace k8s.Models;
public sealed class KubernetesDateTimeYamlConverter : IYamlTypeConverter
{
private static readonly KubernetesDateTimeOffsetYamlConverter OffsetConverter = new();
public bool Accepts(Type type) => type == typeof(DateTime);
public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{
var dto = (DateTimeOffset)OffsetConverter.ReadYaml(parser, typeof(DateTimeOffset), rootDeserializer);
return dto.DateTime;
}
public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer)
{
var date = new DateTimeOffset((DateTime)value);
OffsetConverter.WriteYaml(emitter, date, typeof(DateTimeOffset), serializer);
}
}

View File

@@ -40,24 +40,5 @@ namespace k8s.Models
/// </summary>
[JsonPropertyName("metadata")]
public V1ListMeta Metadata { get; set; }
/// <summary>
/// Validate the object.
/// </summary>
public void Validate()
{
if (Items == null)
{
throw new ArgumentNullException("Items");
}
if (Items != null)
{
foreach (var element in Items.OfType<IValidate>())
{
element.Validate();
}
}
}
}
}

View File

@@ -1,21 +0,0 @@
namespace k8s.Models;
public static class ModelVersionConverter
{
public interface IModelVersionConverter
{
TTo Convert<TFrom, TTo>(TFrom from);
}
public static IModelVersionConverter Converter { get; set; }
internal static TTo Convert<TFrom, TTo>(TFrom from)
{
if (Converter == null)
{
throw new InvalidOperationException("Converter is not set");
}
return Converter.Convert<TFrom, TTo>(from);
}
}

View File

@@ -54,7 +54,7 @@ namespace k8s.Models
/// cause implementors to also use a fixed point implementation.
/// </summary>
[JsonConverter(typeof(ResourceQuantityJsonConverter))]
public partial class ResourceQuantity
public class ResourceQuantity
{
public enum SuffixFormat
{
@@ -97,39 +97,6 @@ namespace k8s.Models
return CanonicalizeString();
}
protected bool Equals(ResourceQuantity other)
{
return _unitlessValue.Equals(other?._unitlessValue);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != GetType())
{
return false;
}
return Equals((ResourceQuantity)obj);
}
public override int GetHashCode()
{
unchecked
{
return ((int)Format * 397) ^ _unitlessValue.GetHashCode();
}
}
//
// CanonicalizeString = go version CanonicalizeBytes
// CanonicalizeBytes returns the canonical form of q and its suffix (see comment on Quantity).
@@ -157,10 +124,9 @@ namespace k8s.Models
return Suffixer.AppendMaxSuffix(_unitlessValue, suffixFormat);
}
// ctor
partial void CustomInit()
public ResourceQuantity(string v)
{
if (Value == null)
if (v == null)
{
// No value has been defined, initialize to 0.
_unitlessValue = new Fraction(0);
@@ -168,7 +134,7 @@ namespace k8s.Models
return;
}
var value = Value.Trim();
var value = v.Trim();
var si = value.IndexOfAny(SuffixChars);
if (si == -1)
@@ -188,6 +154,11 @@ namespace k8s.Models
}
}
public static implicit operator ResourceQuantity(string v)
{
return new ResourceQuantity(v);
}
private static bool HasMantissa(Fraction value)
{
if (value.IsZero)
@@ -200,7 +171,7 @@ namespace k8s.Models
public static implicit operator decimal(ResourceQuantity v)
{
return v?.ToDecimal() ?? 0;
return v.ToDecimal();
}
public static implicit operator ResourceQuantity(decimal v)
@@ -208,6 +179,46 @@ namespace k8s.Models
return new ResourceQuantity(v, 0, SuffixFormat.DecimalExponent);
}
public bool Equals(ResourceQuantity other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return _unitlessValue.Equals(other._unitlessValue);
}
public override bool Equals(object obj)
{
return Equals(obj as ResourceQuantity);
}
public override int GetHashCode()
{
return _unitlessValue.GetHashCode();
}
public static bool operator ==(ResourceQuantity left, ResourceQuantity right)
{
if (left is null)
{
return right is null;
}
return left.Equals(right);
}
public static bool operator !=(ResourceQuantity left, ResourceQuantity right)
{
return !(left == right);
}
private sealed class Suffixer
{
private static readonly IReadOnlyDictionary<string, (int, int)> BinSuffixes =

View File

@@ -8,16 +8,16 @@ namespace k8s.Models
switch (reader.TokenType)
{
case JsonTokenType.Null:
return new ResourceQuantity(null);
return null;
case JsonTokenType.Number:
if (reader.TryGetDouble(out var val))
{
return new ResourceQuantity(Convert.ToString(val));
return Convert.ToString(val);
}
return reader.GetDecimal();
default:
return new ResourceQuantity(reader.GetString());
return reader.GetString();
}
}
@@ -28,7 +28,7 @@ namespace k8s.Models
throw new ArgumentNullException(nameof(writer));
}
writer.WriteStringValue(value?.ToString());
writer.WriteStringValue(value.ToString());
}
}
}

View File

@@ -21,7 +21,7 @@ namespace k8s.Models
return null;
}
return new ResourceQuantity(scalar?.Value);
return scalar?.Value;
}
finally
{

View File

@@ -1,7 +1,7 @@
namespace k8s.Models
{
[JsonConverter(typeof(V1PatchJsonConverter))]
public partial class V1Patch
public record V1Patch
{
public enum PatchType
{
@@ -31,26 +31,21 @@ namespace k8s.Models
ApplyPatch,
}
[JsonPropertyName("content")]
[JsonInclude]
public object Content { get; private set; }
public PatchType Type { get; private set; }
public V1Patch(object body, PatchType type)
{
Content = body;
if (type == PatchType.Unknown)
{
throw new ArgumentException("patch type must be set", nameof(type));
}
Content = body ?? throw new ArgumentNullException(nameof(body), "object must be set");
Type = type;
CustomInit();
}
partial void CustomInit()
{
if (Content == null)
{
throw new ArgumentNullException(nameof(Content), "object must be set");
}
if (Type == PatchType.Unknown)
{
throw new ArgumentException("patch type must be set", nameof(Type));
}
}
}
}

View File

@@ -4,7 +4,7 @@ namespace k8s.Models
/// Partial implementation of the IMetadata interface
/// to open this class up to ModelExtensions methods
/// </summary>
public partial class V1PodTemplateSpec : IMetadata<V1ObjectMeta>
public partial record V1PodTemplateSpec : IMetadata<V1ObjectMeta>
{
}
}

View File

@@ -1,23 +1,28 @@
namespace k8s.Models
{
public partial class V1Status
public partial record V1Status
{
internal sealed class V1StatusObjectViewConverter : JsonConverter<V1Status>
public sealed class V1StatusObjectViewConverter : JsonConverter<V1Status>
{
public override V1Status Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var obj = JsonElement.ParseValue(ref reader);
using var doc = JsonDocument.ParseValue(ref reader);
var ele = doc.RootElement.Clone();
try
{
return obj.Deserialize<V1Status>();
#if NET8_0_OR_GREATER
return JsonSerializer.Deserialize(ele, StatusSourceGenerationContext.Default.V1Status);
#else
return ele.Deserialize<V1Status>();
#endif
}
catch (JsonException)
{
// should be an object
}
return new V1Status { _original = obj, HasObject = true };
return new V1Status { _original = ele, HasObject = true };
}
public override void Write(Utf8JsonWriter writer, V1Status value, JsonSerializerOptions options)
@@ -32,7 +37,11 @@ namespace k8s.Models
public T ObjectView<T>()
{
#if NET8_0_OR_GREATER
return KubernetesJson.Deserialize<T>(_original);
#else
return _original.Deserialize<T>();
#endif
}
}
}

View File

@@ -2,7 +2,7 @@ using System.Net;
namespace k8s.Models
{
public partial class V1Status
public partial record V1Status
{
/// <summary>Converts a <see cref="V1Status"/> object into a short description of the status.</summary>
/// <returns>string description of the status</returns>

View File

@@ -0,0 +1,28 @@
using static k8s.KubernetesJson;
using static k8s.Models.V1Status;
namespace k8s;
[JsonSourceGenerationOptions(
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
UseStringEnumConverter = true,
Converters = new[] { typeof(Iso8601TimeSpanConverter), typeof(KubernetesDateTimeConverter), typeof(KubernetesDateTimeOffsetConverter), typeof(V1StatusObjectViewConverter) })
]
public partial class SourceGenerationContext : JsonSerializerContext
{
}
/// <summary>
/// Used by V1Status in order to avoid the recursive loop as SourceGenerationContext contains V1StatusObjectViewConverter
/// </summary>
[JsonSerializable(typeof(V1Status))]
[JsonSourceGenerationOptions(
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
UseStringEnumConverter = true,
Converters = new[] { typeof(Iso8601TimeSpanConverter), typeof(KubernetesDateTimeConverter), typeof(KubernetesDateTimeOffsetConverter) })
]
public partial class StatusSourceGenerationContext : JsonSerializerContext
{
}

View File

@@ -16,6 +16,7 @@ namespace k8s
/// The action to invoke when the server closes the connection.
/// </param>
/// <returns>a watch object</returns>
[Obsolete("This method will be deprecated in future versions. Please use the Watch method instead.")]
public static Watcher<T> Watch<T, L>(
this Task<HttpOperationResponse<L>> responseTask,
Action<WatchEventType, T> onEvent,
@@ -52,6 +53,7 @@ namespace k8s
/// The action to invoke when the server closes the connection.
/// </param>
/// <returns>a watch object</returns>
[Obsolete("This method will be deprecated in future versions. Please use the Watch method instead.")]
public static Watcher<T> Watch<T, L>(
this HttpOperationResponse<L> response,
Action<WatchEventType, T> onEvent,
@@ -71,6 +73,7 @@ namespace k8s
/// <param name="onError">a callback when any exception was caught during watching</param>
/// <param name="cancellationToken">cancellation token</param>
/// <returns>IAsyncEnumerable of watch events</returns>
[Obsolete("This method will be deprecated in future versions. Please use the WatchAsync method instead.")]
public static IAsyncEnumerable<(WatchEventType, T)> WatchAsync<T, L>(
this Task<HttpOperationResponse<L>> responseTask,
Action<Exception> onError = null,

View File

@@ -71,7 +71,7 @@ namespace LibKubernetesGenerator
sc = scriptObjectFactory.CreateScriptObject();
sc.SetValue("groups", groups, true);
context.RenderToContext($"IBasicKubernetes.cs.template", sc, $"IBasicKubernetes.g.cs");
context.RenderToContext($"IKubernetes.cs.template", sc, $"IKubernetes.g.cs");
context.RenderToContext($"AbstractKubernetes.cs.template", sc, $"AbstractKubernetes.g.cs");
}
}

View File

@@ -78,6 +78,12 @@ namespace LibKubernetesGenerator
}
if (definition.Format == "int-or-string")
{
return "IntOrString";
}
return schemaToNameMapCooked[definition];
}
}

View File

@@ -57,8 +57,6 @@ namespace LibKubernetesGenerator
}
}
interfaces.Add("IValidate");
return string.Join(", ", interfaces);
}
@@ -68,7 +66,7 @@ namespace LibKubernetesGenerator
if (init == "true" && !parameter.IsRequired)
{
name += " = null";
name += " = default";
}
return name;

View File

@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.CSharp;
using Scriban;
using Scriban.Runtime;
using System.Text;
@@ -19,7 +20,10 @@ namespace LibKubernetesGenerator
{
var template = Template.Parse(EmbedResource.GetResource(templatefile));
var generated = template.Render(tc);
context.AddSource(generatedfile, SourceText.From(generated, Encoding.UTF8));
var syntaxTree = CSharpSyntaxTree.ParseText(generated);
var normalized = syntaxTree.GetRoot().NormalizeWhitespace().ToFullString();
context.AddSource(generatedfile, SourceText.From(normalized, Encoding.UTF8));
}
}
}

View File

@@ -58,12 +58,10 @@ namespace LibKubernetesGenerator
builder.RegisterType<ScriptObjectFactory>()
;
builder.RegisterType<ModelExtGenerator>();
builder.RegisterType<SourceGenerationContextGenerator>();
builder.RegisterType<ModelGenerator>();
builder.RegisterType<ApiGenerator>();
builder.RegisterType<ClientSetGenerator>();
builder.RegisterType<VersionConverterStubGenerator>();
builder.RegisterType<VersionGenerator>();
return builder.Build();
@@ -79,9 +77,7 @@ namespace LibKubernetesGenerator
container.Resolve<VersionGenerator>().Generate(swagger, ctx);
container.Resolve<ModelGenerator>().Generate(swagger, ctx);
container.Resolve<ModelExtGenerator>().Generate(swagger, ctx);
container.Resolve<SourceGenerationContextGenerator>().Generate(swagger, ctx);
container.Resolve<VersionConverterStubGenerator>().Generate(swagger, ctx);
container.Resolve<ApiGenerator>().Generate(swagger, ctx);
container.Resolve<ClientSetGenerator>().Generate(swagger, ctx);
});

View File

@@ -13,6 +13,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,36 +0,0 @@
using Microsoft.CodeAnalysis;
using NSwag;
using System.Collections.Generic;
using System.Linq;
namespace LibKubernetesGenerator
{
internal class ModelExtGenerator
{
private readonly ClassNameHelper classNameHelper;
private readonly ScriptObjectFactory scriptObjectFactory;
public ModelExtGenerator(ClassNameHelper classNameHelper, ScriptObjectFactory scriptObjectFactory)
{
this.classNameHelper = classNameHelper;
this.scriptObjectFactory = scriptObjectFactory;
}
public void Generate(OpenApiDocument swagger, IncrementalGeneratorPostInitializationContext context)
{
// Generate the interface declarations
var skippedTypes = new HashSet<string> { "V1WatchEvent" };
var definitions = swagger.Definitions.Values
.Where(
d => d.ExtensionData != null
&& d.ExtensionData.ContainsKey("x-kubernetes-group-version-kind")
&& !skippedTypes.Contains(classNameHelper.GetClassName(d)));
var sc = scriptObjectFactory.CreateScriptObject();
sc.SetValue("definitions", definitions, true);
context.RenderToContext("ModelExtensions.cs.template", sc, "ModelExtensions.g.cs");
}
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using NSwag;
@@ -18,15 +19,49 @@ namespace LibKubernetesGenerator
{
var sc = scriptObjectFactory.CreateScriptObject();
var genSkippedTypes = new HashSet<string>
{
"IntOrString",
"ResourceQuantity",
"V1Patch",
};
var extSkippedTypes = new HashSet<string>
{
"V1WatchEvent",
};
var typeOverrides = new Dictionary<string, string>
{
// not used at the moment
};
foreach (var kv in swagger.Definitions)
{
var def = kv.Value;
var clz = classNameHelper.GetClassNameForSchemaDefinition(def);
if (genSkippedTypes.Contains(clz))
{
continue;
}
var hasExt = def.ExtensionData != null
&& def.ExtensionData.ContainsKey("x-kubernetes-group-version-kind")
&& !extSkippedTypes.Contains(clz);
var typ = "record";
if (typeOverrides.TryGetValue(clz, out var to))
{
typ = to;
}
sc.SetValue("clz", clz, true);
sc.SetValue("def", def, true);
sc.SetValue("properties", def.Properties.Values, true);
sc.SetValue("typ", typ, true);
sc.SetValue("hasExt", hasExt, true);
context.RenderToContext("Model.cs.template", sc, $"Models_{clz}.g.cs");
}

View File

@@ -3,6 +3,7 @@ using NSwag;
using Scriban.Runtime;
using System;
using System.Linq;
using System.Collections.Generic;
namespace LibKubernetesGenerator
{
@@ -21,6 +22,8 @@ namespace LibKubernetesGenerator
{
scriptObject.Import(nameof(GetModelCtorParam), new Func<JsonSchema, string>(GetModelCtorParam));
scriptObject.Import(nameof(IfParamContains), IfParamContains);
scriptObject.Import(nameof(FilterParameters), FilterParameters);
scriptObject.Import(nameof(GetParameterValueForWatch), new Func<OpenApiParameter, bool, string, string>(GetParameterValueForWatch));
}
public static bool IfParamContains(OpenApiOperation operation, string name)
@@ -39,6 +42,23 @@ namespace LibKubernetesGenerator
return found;
}
public static IEnumerable<OpenApiParameter> FilterParameters(OpenApiOperation operation, string excludeParam)
{
return operation.Parameters.Where(p => p.Name != excludeParam);
}
public string GetParameterValueForWatch(OpenApiParameter parameter, bool watch, string init = "false")
{
if (parameter.Name == "watch")
{
return watch ? "true" : "false";
}
else
{
return generalNameHelper.GetDotNetNameOpenApiParameter(parameter, init);
}
}
public string GetModelCtorParam(JsonSchema schema)
{
return string.Join(", ", schema.Properties.Values
@@ -57,4 +77,4 @@ namespace LibKubernetesGenerator
}));
}
}
}
}

View File

@@ -11,11 +11,12 @@ namespace LibKubernetesGenerator
{
private readonly Dictionary<string, string> _classNameToPluralMap;
private readonly ClassNameHelper classNameHelper;
private readonly HashSet<string> opblackList = new HashSet<string>()
{
private readonly HashSet<string> opblackList =
[
"listClusterCustomObject",
"listNamespacedCustomObject",
};
"listCustomObjectForAllNamespaces",
];
public PluralHelper(ClassNameHelper classNameHelper, OpenApiDocument swagger)
{

View File

@@ -122,7 +122,6 @@ namespace LibKubernetesGenerator
return $"IDictionary<string, {GetDotNetType(schema.AdditionalPropertiesSchema, parent)}>";
}
if (schema?.Reference != null)
{
return classNameHelper.GetClassNameForSchemaDefinition(schema.Reference);
@@ -245,6 +244,16 @@ namespace LibKubernetesGenerator
}
break;
case "T":
var itemType = TryGetItemTypeFromSchema(response);
if (itemType != null)
{
return itemType;
}
break;
case "TList":
return t;
}
return t;
@@ -283,5 +292,26 @@ namespace LibKubernetesGenerator
return false;
}
private string TryGetItemTypeFromSchema(OpenApiResponse response)
{
var listSchema = response?.Schema?.Reference;
if (listSchema?.Properties?.TryGetValue("items", out var itemsProperty) != true)
{
return null;
}
if (itemsProperty.Reference != null)
{
return classNameHelper.GetClassNameForSchemaDefinition(itemsProperty.Reference);
}
if (itemsProperty.Item?.Reference != null)
{
return classNameHelper.GetClassNameForSchemaDefinition(itemsProperty.Item.Reference);
}
return null;
}
}
}
}

View File

@@ -1,70 +0,0 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using NSwag;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace LibKubernetesGenerator
{
internal class VersionConverterStubGenerator
{
private readonly ClassNameHelper classNameHelper;
public VersionConverterStubGenerator(ClassNameHelper classNameHelper)
{
this.classNameHelper = classNameHelper;
}
public void Generate(OpenApiDocument swagger, IncrementalGeneratorPostInitializationContext context)
{
var allGeneratedModelClassNames = new List<string>();
foreach (var kv in swagger.Definitions)
{
var def = kv.Value;
var clz = classNameHelper.GetClassNameForSchemaDefinition(def);
allGeneratedModelClassNames.Add(clz);
}
var versionRegex = @"(^V|v)[0-9]+((alpha|beta)[0-9]+)?";
var typePairs = allGeneratedModelClassNames
.OrderBy(x => x)
.Select(x => new
{
Version = Regex.Match(x, versionRegex).Value?.ToLower(),
Kinda = Regex.Replace(x, versionRegex, string.Empty),
Type = x,
})
.Where(x => !string.IsNullOrEmpty(x.Version))
.GroupBy(x => x.Kinda)
.Where(x => x.Count() > 1)
.SelectMany(x =>
x.SelectMany((value, index) => x.Skip(index + 1), (first, second) => new { first, second }))
.OrderBy(x => x.first.Kinda)
.ThenBy(x => x.first.Version)
.Select(x => (x.first.Type, x.second.Type))
.ToList();
var sbmodel = new StringBuilder(@"// <auto-generated />
namespace k8s.Models;
");
foreach (var (t0, t1) in typePairs)
{
sbmodel.AppendLine($@"
public partial class {t0}
{{
public static explicit operator {t0}({t1} s) => ModelVersionConverter.Convert<{t1}, {t0}>(s);
}}
public partial class {t1}
{{
public static explicit operator {t1}({t0} s) => ModelVersionConverter.Convert<{t0}, {t1}>(s);
}}");
}
context.AddSource($"ModelOperators.g.cs", SourceText.From(sbmodel.ToString(), Encoding.UTF8));
}
}
}

View File

@@ -17,10 +17,11 @@ public partial class {{name}}Client : ResourceClient
}
{{for api in apis }}
{{~ $filteredParams = FilterParameters api.operation "watch" ~}}
/// <summary>
/// {{ToXmlDoc api.operation.description}}
/// </summary>
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
/// <param name="{{GetDotNetNameOpenApiParameter parameter "false"}}">
/// {{ToXmlDoc parameter.description}}
/// </param>
@@ -29,7 +30,7 @@ public partial class {{name}}Client : ResourceClient
/// A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation.
/// </param>
public async Task{{GetReturnType api.operation "<>"}} {{GetActionName api.operation name "Async"}}(
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
{{GetDotNetTypeOpenApiParameter parameter}} {{GetDotNetNameOpenApiParameter parameter "true"}},
{{ end }}
CancellationToken cancellationToken = default(CancellationToken))
@@ -37,7 +38,7 @@ public partial class {{name}}Client : ResourceClient
{{if IfReturnType api.operation "stream"}}
var _result = await Client.{{group}}.{{GetOperationId api.operation "WithHttpMessagesAsync"}}(
{{ for parameter in api.operation.parameters}}
{{GetDotNetNameOpenApiParameter parameter "false"}},
{{GetParameterValueForWatch parameter false}},
{{end}}
null,
cancellationToken);
@@ -47,7 +48,7 @@ public partial class {{name}}Client : ResourceClient
{{if IfReturnType api.operation "obj"}}
using (var _result = await Client.{{group}}.{{GetOperationId api.operation "WithHttpMessagesAsync"}}(
{{ for parameter in api.operation.parameters}}
{{GetDotNetNameOpenApiParameter parameter "false"}},
{{GetParameterValueForWatch parameter false}},
{{end}}
null,
cancellationToken).ConfigureAwait(false))
@@ -58,7 +59,7 @@ public partial class {{name}}Client : ResourceClient
{{if IfReturnType api.operation "void"}}
using (var _result = await Client.{{group}}.{{GetOperationId api.operation "WithHttpMessagesAsync"}}(
{{ for parameter in api.operation.parameters}}
{{GetDotNetNameOpenApiParameter parameter "false"}},
{{GetParameterValueForWatch parameter false}},
{{end}}
null,
cancellationToken).ConfigureAwait(false))
@@ -71,7 +72,7 @@ public partial class {{name}}Client : ResourceClient
/// <summary>
/// {{ToXmlDoc api.operation.description}}
/// </summary>
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
/// <param name="{{GetDotNetNameOpenApiParameter parameter "false"}}">
/// {{ToXmlDoc parameter.description}}
/// </param>
@@ -80,14 +81,14 @@ public partial class {{name}}Client : ResourceClient
/// A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation.
/// </param>
public async Task<T> {{GetActionName api.operation name "Async"}}<T>(
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
{{GetDotNetTypeOpenApiParameter parameter}} {{GetDotNetNameOpenApiParameter parameter "false"}},
{{ end }}
CancellationToken cancellationToken = default(CancellationToken))
{
using (var _result = await Client.{{group}}.{{GetOperationId api.operation "WithHttpMessagesAsync"}}<T>(
{{ for parameter in api.operation.parameters}}
{{GetDotNetNameOpenApiParameter parameter "false"}},
{{GetParameterValueForWatch parameter false}},
{{end}}
null,
cancellationToken).ConfigureAwait(false))
@@ -96,5 +97,69 @@ public partial class {{name}}Client : ResourceClient
}
}
{{end}}
#if !K8S_AOT
{{if IfParamContains api.operation "watch"}}
/// <summary>
/// Watch {{ToXmlDoc api.operation.description}}
/// </summary>
{{ for parameter in $filteredParams}}
/// <param name="{{GetDotNetNameOpenApiParameter parameter "false"}}">
/// {{ToXmlDoc parameter.description}}
/// </param>
{{ end }}
/// <param name="onEvent">Callback when any event raised from api server</param>
/// <param name="onError">Callback when any exception was caught during watching</param>
/// <param name="onClosed">Callback when the server closes the connection</param>
public Watcher<{{GetReturnType api.operation "T"}}> Watch{{GetActionName api.operation name ""}}(
{{ for parameter in $filteredParams}}
{{GetDotNetTypeOpenApiParameter parameter}} {{GetDotNetNameOpenApiParameter parameter "true"}},
{{ end }}
Action<WatchEventType, {{GetReturnType api.operation "T"}}> onEvent = null,
Action<Exception> onError = null,
Action onClosed = null)
{
if (onEvent == null) throw new ArgumentNullException(nameof(onEvent));
var responseTask = Client.{{group}}.{{GetOperationId api.operation "WithHttpMessagesAsync"}}(
{{ for parameter in api.operation.parameters}}
{{GetParameterValueForWatch parameter true}},
{{ end }}
null,
CancellationToken.None);
return responseTask.Watch<{{GetReturnType api.operation "T"}}, {{GetReturnType api.operation "TList"}}>(
onEvent, onError, onClosed);
}
/// <summary>
/// Watch {{ToXmlDoc api.operation.description}} as async enumerable
/// </summary>
{{ for parameter in $filteredParams}}
/// <param name="{{GetDotNetNameOpenApiParameter parameter "false"}}">
/// {{ToXmlDoc parameter.description}}
/// </param>
{{ end }}
/// <param name="onError">Callback when any exception was caught during watching</param>
/// <param name="cancellationToken">Cancellation token</param>
public IAsyncEnumerable<(WatchEventType, {{GetReturnType api.operation "T"}})> Watch{{GetActionName api.operation name "Async"}}(
{{ for parameter in $filteredParams}}
{{GetDotNetTypeOpenApiParameter parameter}} {{GetDotNetNameOpenApiParameter parameter "true"}},
{{ end }}
Action<Exception> onError = null,
CancellationToken cancellationToken = default)
{
var responseTask = Client.{{group}}.{{GetOperationId api.operation "WithHttpMessagesAsync"}}(
{{ for parameter in api.operation.parameters}}
{{GetParameterValueForWatch parameter true}},
{{ end }}
null,
cancellationToken);
return responseTask.WatchAsync<{{GetReturnType api.operation "T"}}, {{GetReturnType api.operation "TList"}}>(
onError, cancellationToken);
}
{{end}}
}
#endif
{{end}}
}

View File

@@ -8,7 +8,7 @@ namespace k8s;
/// <summary>
/// </summary>
public partial interface IBasicKubernetes
public partial interface IKubernetes
{
{{for group in groups}}
I{{group}}Operations {{group}} { get; }

View File

@@ -4,92 +4,29 @@
// regenerated.
// </auto-generated>
namespace k8s.Models
namespace k8s.Models;
/// <summary>
/// {{ToXmlDoc def.description}}
/// </summary>
{{ if hasExt }}
[KubernetesEntity(Group=KubeGroup, Kind=KubeKind, ApiVersion=KubeApiVersion, PluralName=KubePluralName)]
{{ end }}
public partial {{typ}} {{clz}} {{ if hasExt }} : {{ GetInterfaceName def }} {{ end }}
{
{{ if hasExt}}
public const string KubeApiVersion = "{{ GetApiVersion def }}";
public const string KubeKind = "{{ GetKind def }}";
public const string KubeGroup = "{{ GetGroup def }}";
public const string KubePluralName = "{{ GetPlural def }}";
{{ end }}
{{ for property in properties }}
/// <summary>
/// {{ToXmlDoc def.description}}
/// {{ToXmlDoc property.description}}
/// </summary>
public partial class {{clz}}
{
/// <summary>
/// Initializes a new instance of the {{GetClassName def}} class.
/// </summary>
public {{clz}}()
{
CustomInit();
}
/// <summary>
/// Initializes a new instance of the {{GetClassName def}} class.
/// </summary>
{{ for property in properties }}
{{ if property.IsRequired }}
/// <param name="{{GetDotNetName property.name "fieldctor"}}">
/// {{ToXmlDoc property.description}}
/// </param>
{{ end }}
{{ end }}
{{ for property in properties }}
{{ if !property.IsRequired }}
/// <param name="{{GetDotNetName property.name "fieldctor"}}">
/// {{ToXmlDoc property.description}}
/// </param>
{{ end }}
{{ end }}
public {{clz}}({{GetModelCtorParam def}})
{
{{ for property in properties }}
{{GetDotNetName property.name "field"}} = {{GetDotNetName property.name "fieldctor"}};
{{ end }}
CustomInit();
}
/// <summary>
/// An initialization method that performs custom operations like setting defaults
/// </summary>
partial void CustomInit();
{{ for property in properties }}
/// <summary>
/// {{ToXmlDoc property.description}}
/// </summary>
[JsonPropertyName("{{property.name}}")]
public {{GetDotNetType property}} {{GetDotNetName property.name "field"}} { get; set; }
{{ end }}
/// <summary>
/// Validate the object.
/// </summary>
public virtual void Validate()
{
{{ for property in properties }}
{{if IfType property "object" }}
{{ if property.IsRequired }}
if ({{GetDotNetName property.name "field"}} == null)
{
throw new ArgumentNullException("{{GetDotNetName property.name "field"}}");
}
{{ end }}
{{ end }}
{{ end }}
{{ for property in properties }}
{{if IfType property "object" }}
{{GetDotNetName property.name "field"}}?.Validate();
{{ end }}
{{if IfType property "objectarray" }}
if ({{GetDotNetName property.name "field"}} != null){
foreach(var obj in {{GetDotNetName property.name "field"}})
{
obj.Validate();
}
}
{{ end }}
{{ end }}
}
}
[JsonPropertyName("{{property.name}}")]
public {{ if property.IsRequired }} required {{ end }} {{GetDotNetType property}} {{GetDotNetName property.name "field"}} { get; set; }
{{ end }}
}

View File

@@ -1,17 +0,0 @@
// <auto-generated>
// Code generated by https://github.com/kubernetes-client/csharp/tree/master/src/LibKubernetesGenerator
// Changes may cause incorrect behavior and will be lost if the code is regenerated.
// </auto-generated>
namespace k8s.Models
{
{{ for definition in definitions }}
[KubernetesEntity(Group=KubeGroup, Kind=KubeKind, ApiVersion=KubeApiVersion, PluralName=KubePluralName)]
public partial class {{ GetClassName definition }} : {{ GetInterfaceName definition }}
{
public const string KubeApiVersion = "{{ GetApiVersion definition }}";
public const string KubeKind = "{{ GetKind definition }}";
public const string KubeGroup = "{{ GetGroup definition }}";
public const string KubePluralName = "{{ GetPlural definition }}";
}
{{ end }}
}

View File

@@ -12,26 +12,27 @@ namespace k8s;
public static partial class {{name}}OperationsExtensions
{
{{for api in apis }}
{{~ $filteredParams = FilterParameters api.operation "watch" ~}}
/// <summary>
/// {{ToXmlDoc api.operation.description}}
/// </summary>
/// <param name='operations'>
/// The operations group for this extension method.
/// </param>
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
/// <param name="{{GetDotNetNameOpenApiParameter parameter "false"}}">
/// {{ToXmlDoc api.description}}
/// {{ToXmlDoc parameter.description}}
/// </param>
{{ end }}
public static {{GetReturnType api.operation "void"}} {{GetOperationId api.operation ""}}(
this I{{name}}Operations operations
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
,{{GetDotNetTypeOpenApiParameter parameter}} {{GetDotNetNameOpenApiParameter parameter "true"}}
{{end}}
)
{
{{GetReturnType api.operation "return"}} operations.{{GetOperationId api.operation "Async"}}(
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
{{GetDotNetNameOpenApiParameter parameter "false"}},
{{end}}
CancellationToken.None
@@ -45,20 +46,20 @@ public static partial class {{name}}OperationsExtensions
/// <param name='operations'>
/// The operations group for this extension method.
/// </param>
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
/// <param name="{{GetDotNetNameOpenApiParameter parameter "false"}}">
/// {{ToXmlDoc parameter.description}}
/// </param>
{{end}}
public static T {{GetOperationId api.operation ""}}<T>(
this I{{name}}Operations operations
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
,{{GetDotNetTypeOpenApiParameter parameter}} {{GetDotNetNameOpenApiParameter parameter "true"}}
{{end}}
)
{
return operations.{{GetOperationId api.operation "Async"}}<T>(
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
{{GetDotNetNameOpenApiParameter parameter "false"}},
{{end}}
CancellationToken.None
@@ -72,7 +73,7 @@ public static partial class {{name}}OperationsExtensions
/// <param name='operations'>
/// The operations group for this extension method.
/// </param>
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
/// <param name="{{GetDotNetNameOpenApiParameter parameter "false"}}">
/// {{ToXmlDoc parameter.description}}
/// </param>
@@ -82,7 +83,7 @@ public static partial class {{name}}OperationsExtensions
/// </param>
public static async Task{{GetReturnType api.operation "<>"}} {{GetOperationId api.operation "Async"}}(
this I{{name}}Operations operations,
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
{{GetDotNetTypeOpenApiParameter parameter}} {{GetDotNetNameOpenApiParameter parameter "true"}},
{{ end }}
CancellationToken cancellationToken = default(CancellationToken))
@@ -90,7 +91,7 @@ public static partial class {{name}}OperationsExtensions
{{if IfReturnType api.operation "stream"}}
var _result = await operations.{{GetOperationId api.operation "WithHttpMessagesAsync"}}(
{{ for parameter in api.operation.parameters}}
{{GetDotNetNameOpenApiParameter parameter "false"}},
{{GetParameterValueForWatch parameter false}},
{{end}}
null,
cancellationToken);
@@ -100,7 +101,7 @@ public static partial class {{name}}OperationsExtensions
{{if IfReturnType api.operation "obj"}}
using (var _result = await operations.{{GetOperationId api.operation "WithHttpMessagesAsync"}}(
{{ for parameter in api.operation.parameters}}
{{GetDotNetNameOpenApiParameter parameter "false"}},
{{GetParameterValueForWatch parameter false}},
{{end}}
null,
cancellationToken).ConfigureAwait(false))
@@ -111,7 +112,7 @@ public static partial class {{name}}OperationsExtensions
{{if IfReturnType api.operation "void"}}
using (var _result = await operations.{{GetOperationId api.operation "WithHttpMessagesAsync"}}(
{{ for parameter in api.operation.parameters}}
{{GetDotNetNameOpenApiParameter parameter "false"}},
{{GetParameterValueForWatch parameter false}},
{{end}}
null,
cancellationToken).ConfigureAwait(false))
@@ -127,7 +128,7 @@ public static partial class {{name}}OperationsExtensions
/// <param name='operations'>
/// The operations group for this extension method.
/// </param>
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
/// <param name="{{GetDotNetNameOpenApiParameter parameter "false"}}">
/// {{ToXmlDoc parameter.description}}
/// </param>
@@ -137,14 +138,14 @@ public static partial class {{name}}OperationsExtensions
/// </param>
public static async Task<T> {{GetOperationId api.operation "Async"}}<T>(
this I{{name}}Operations operations,
{{ for parameter in api.operation.parameters}}
{{ for parameter in $filteredParams}}
{{GetDotNetTypeOpenApiParameter parameter}} {{GetDotNetNameOpenApiParameter parameter "true"}},
{{ end }}
CancellationToken cancellationToken = default(CancellationToken))
{
using (var _result = await operations.{{GetOperationId api.operation "WithHttpMessagesAsync"}}<T>(
{{ for parameter in api.operation.parameters}}
{{GetDotNetNameOpenApiParameter parameter "false"}},
{{GetParameterValueForWatch parameter false}},
{{end}}
null,
cancellationToken).ConfigureAwait(false))
@@ -154,5 +155,77 @@ public static partial class {{name}}OperationsExtensions
}
{{end}}
{{end}}
#if !K8S_AOT
{{if IfParamContains api.operation "watch"}}
{{~ $filteredParams = FilterParameters api.operation "watch" ~}}
/// <summary>
/// Watch {{ToXmlDoc api.operation.description}}
/// </summary>
/// <param name='operations'>
/// The operations group for this extension method.
/// </param>
{{ for parameter in $filteredParams}}
/// <param name="{{GetDotNetNameOpenApiParameter parameter "false"}}">
/// {{ToXmlDoc parameter.description}}
/// </param>
{{ end }}
/// <param name="onEvent">Callback when any event raised from api server</param>
/// <param name="onError">Callback when any exception was caught during watching</param>
/// <param name="onClosed">Callback when the server closes the connection</param>
public static Watcher<{{GetReturnType api.operation "T"}}> Watch{{GetOperationId api.operation ""}}(
this I{{name}}Operations operations,
{{ for parameter in $filteredParams}}
{{GetDotNetTypeOpenApiParameter parameter}} {{GetDotNetNameOpenApiParameter parameter "true"}},
{{end}}
Action<WatchEventType, {{GetReturnType api.operation "T"}}> onEvent = null,
Action<Exception> onError = null,
Action onClosed = null)
{
if (onEvent == null) throw new ArgumentNullException(nameof(onEvent));
var responseTask = operations.{{GetOperationId api.operation "WithHttpMessagesAsync"}}(
{{ for parameter in api.operation.parameters}}
{{GetParameterValueForWatch parameter true}},
{{end}}
null,
CancellationToken.None);
return responseTask.Watch<{{GetReturnType api.operation "T"}}, {{GetReturnType api.operation "TList"}}>(
onEvent, onError, onClosed);
}
/// <summary>
/// Watch {{ToXmlDoc api.operation.description}} as async enumerable
/// </summary>
/// <param name='operations'>
/// The operations group for this extension method.
/// </param>
{{ for parameter in $filteredParams}}
/// <param name="{{GetDotNetNameOpenApiParameter parameter "false"}}">
/// {{ToXmlDoc parameter.description}}
/// </param>
{{ end }}
/// <param name="onError">Callback when any exception was caught during watching</param>
/// <param name="cancellationToken">Cancellation token</param>
public static IAsyncEnumerable<(WatchEventType, {{GetReturnType api.operation "T"}})> Watch{{GetOperationId api.operation "Async"}}(
this I{{name}}Operations operations,
{{ for parameter in $filteredParams}}
{{GetDotNetTypeOpenApiParameter parameter}} {{GetDotNetNameOpenApiParameter parameter "true"}},
{{end}}
Action<Exception> onError = null,
CancellationToken cancellationToken = default)
{
var responseTask = operations.{{GetOperationId api.operation "WithHttpMessagesAsync"}}(
{{ for parameter in api.operation.parameters}}
{{GetParameterValueForWatch parameter true}},
{{end}}
null,
cancellationToken);
return responseTask.WatchAsync<{{GetReturnType api.operation "T"}}, {{GetReturnType api.operation "TList"}}>(
onError, cancellationToken);
}
{{end}}
#endif
{{end}}
}

View File

@@ -8,8 +8,8 @@ namespace k8s
{{ for definition in definitions }}
[JsonSerializable(typeof({{ GetClassName definition }}))]
{{ end }}
internal partial class SourceGenerationContext : JsonSerializerContext
public partial class SourceGenerationContext : JsonSerializerContext
{
}
}
#endif
#endif

11715
swagger.json

File diff suppressed because it is too large Load Diff

View File

@@ -172,27 +172,33 @@ namespace k8s.E2E
using var kubernetes = CreateClient();
await kubernetes.CoreV1.CreateNamespacedEventAsync(
new Corev1Event(
new V1ObjectReference(
"v1alpha1",
kind: "Test",
name: "test",
namespaceProperty: "default",
resourceVersion: "1",
uid: "1"),
new V1ObjectMeta()
new Corev1Event
{
InvolvedObject = new V1ObjectReference
{
ApiVersion = "v1alpha1",
Kind = "Test",
Name = "test",
NamespaceProperty = "default",
ResourceVersion = "1",
Uid = "1",
},
Metadata = new V1ObjectMeta
{
GenerateName = "started-",
},
action: "STARTED",
type: "Normal",
reason: "STARTED",
message: "Started",
eventTime: DateTime.Now,
firstTimestamp: DateTime.Now,
lastTimestamp: DateTime.Now,
reportingComponent: "37",
reportingInstance: "38"), "default").ConfigureAwait(false);
Action = "STARTED",
Type = "Normal",
Reason = "STARTED",
Message = "Started",
EventTime = DateTime.Now,
FirstTimestamp = DateTime.Now,
LastTimestamp = DateTime.Now,
ReportingComponent = "37",
ReportingInstance = "38",
},
"default"
).ConfigureAwait(false);
}
[MinikubeFact]
@@ -370,7 +376,7 @@ namespace k8s.E2E
}
}
[MinikubeFact]
public async Task VersionTestAsync()
{

View File

@@ -224,13 +224,12 @@ namespace k8s.E2E
var started = new AsyncManualResetEvent();
var connectionClosed = new AsyncManualResetEvent();
var watcher = kubernetes.BatchV1.ListNamespacedJobWithHttpMessagesAsync(
var watcher = kubernetes.BatchV1.WatchListNamespacedJob(
job.Metadata.NamespaceProperty,
fieldSelector: $"metadata.name={job.Metadata.Name}",
resourceVersion: job.Metadata.ResourceVersion,
timeoutSeconds: 30,
watch: true).Watch<V1Job, V1JobList>(
(type, source) =>
onEvent: (type, source) =>
{
Debug.WriteLine($"Watcher 1: {type}, {source}");
events.Add(new Tuple<WatchEventType, V1Job>(type, source));
@@ -250,6 +249,86 @@ namespace k8s.E2E
new V1DeleteOptions() { PropagationPolicy = "Foreground" }).ConfigureAwait(false);
}
[MinikubeFact]
public async Task WatcherIntegrationTestAsyncEnumerable()
{
using var kubernetes = CreateClient();
var job = await kubernetes.BatchV1.CreateNamespacedJobAsync(
new V1Job()
{
ApiVersion = "batch/v1",
Kind = V1Job.KubeKind,
Metadata = new V1ObjectMeta() { Name = nameof(WatcherIntegrationTestAsyncEnumerable).ToLowerInvariant() },
Spec = new V1JobSpec()
{
Template = new V1PodTemplateSpec()
{
Spec = new V1PodSpec()
{
Containers = new List<V1Container>()
{
new V1Container()
{
Image = "ubuntu",
Name = "runner",
Command = new List<string>() { "/bin/bash", "-c", "--" },
Args = new List<string>()
{
"trap : TERM INT; sleep infinity & wait",
},
},
},
RestartPolicy = "Never",
},
},
},
},
"default").ConfigureAwait(false);
var events = new Collection<Tuple<WatchEventType, V1Job>>();
var started = new AsyncManualResetEvent();
var watchCompleted = new AsyncManualResetEvent();
// Start async enumerable watch in background task to mimic callback behavior
var watchTask = Task.Run(async () =>
{
try
{
await foreach (var (type, source) in kubernetes.BatchV1.WatchListNamespacedJobAsync(
job.Metadata.NamespaceProperty,
fieldSelector: $"metadata.name={job.Metadata.Name}",
resourceVersion: job.Metadata.ResourceVersion,
timeoutSeconds: 30).ConfigureAwait(false))
{
Debug.WriteLine($"AsyncEnumerable Watcher: {type}, {source}");
events.Add(new Tuple<WatchEventType, V1Job>(type, source));
job = source;
started.Set();
}
}
catch (Exception ex)
{
Debug.WriteLine($"Watch exception: {ex.GetType().FullName}: {ex.Message}");
}
finally
{
watchCompleted.Set();
}
});
await started.WaitAsync().ConfigureAwait(false);
await Task.WhenAny(watchCompleted.WaitAsync(), Task.Delay(TimeSpan.FromMinutes(3))).ConfigureAwait(false);
Assert.True(watchCompleted.IsSet);
var st = await kubernetes.BatchV1.DeleteNamespacedJobAsync(
job.Metadata.Name,
job.Metadata.NamespaceProperty,
new V1DeleteOptions() { PropagationPolicy = "Foreground" }).ConfigureAwait(false);
}
[MinikubeFact]
public void LeaderIntegrationTest()
{
@@ -451,27 +530,33 @@ namespace k8s.E2E
using var kubernetes = CreateClient();
await kubernetes.CoreV1.CreateNamespacedEventAsync(
new Corev1Event(
new V1ObjectReference(
"v1alpha1",
kind: "Test",
name: "test",
namespaceProperty: "default",
resourceVersion: "1",
uid: "1"),
new V1ObjectMeta()
new Corev1Event
{
Metadata = new V1ObjectMeta
{
GenerateName = "started-",
NamespaceProperty = "default",
},
action: "STARTED",
type: "Normal",
reason: "STARTED",
message: "Started",
eventTime: DateTime.Now,
firstTimestamp: DateTime.Now,
lastTimestamp: DateTime.Now,
reportingComponent: "37",
reportingInstance: "38"), "default").ConfigureAwait(false);
InvolvedObject = new V1ObjectReference
{
ApiVersion = "v1alpha1",
Kind = "Test",
Name = "test",
NamespaceProperty = "default",
ResourceVersion = "1",
Uid = "1",
},
Action = "STARTED",
Type = "Normal",
Reason = "STARTED",
Message = "Started",
EventTime = DateTime.Now,
FirstTimestamp = DateTime.Now,
LastTimestamp = DateTime.Now,
ReportingComponent = "37",
ReportingInstance = "38",
},
"default").ConfigureAwait(false);
}
[MinikubeFact]
@@ -873,6 +958,171 @@ namespace k8s.E2E
}
}
[MinikubeFact]
public async Task V2HorizontalPodAutoscalerTestAsync()
{
var namespaceParameter = "default";
var deploymentName = "k8scsharp-e2e-hpa-deployment";
var hpaName = "k8scsharp-e2e-hpa";
using var client = CreateClient();
async Task CleanupAsync()
{
var deleteOptions = new V1DeleteOptions { PropagationPolicy = "Foreground" };
try
{
await client.AutoscalingV2.DeleteNamespacedHorizontalPodAutoscalerAsync(hpaName, namespaceParameter, deleteOptions).ConfigureAwait(false);
}
catch (HttpOperationException e)
{
if (e.Response?.StatusCode != System.Net.HttpStatusCode.NotFound)
{
throw;
}
}
try
{
await client.AppsV1.DeleteNamespacedDeploymentAsync(deploymentName, namespaceParameter, deleteOptions).ConfigureAwait(false);
}
catch (HttpOperationException e)
{
if (e.Response?.StatusCode != System.Net.HttpStatusCode.NotFound)
{
throw;
}
}
var attempts = 10;
while (attempts-- > 0)
{
var hpaList = await client.AutoscalingV2.ListNamespacedHorizontalPodAutoscalerAsync(namespaceParameter).ConfigureAwait(false);
var deploymentList = await client.AppsV1.ListNamespacedDeploymentAsync(namespaceParameter).ConfigureAwait(false);
if (hpaList.Items.All(item => item.Metadata.Name != hpaName) && deploymentList.Items.All(item => item.Metadata.Name != deploymentName))
{
break;
}
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
}
}
try
{
await CleanupAsync().ConfigureAwait(false);
var labels = new Dictionary<string, string> { ["app"] = "k8scsharp-hpa" };
await client.AppsV1.CreateNamespacedDeploymentAsync(
new V1Deployment
{
Metadata = new V1ObjectMeta { Name = deploymentName, Labels = new Dictionary<string, string>(labels) },
Spec = new V1DeploymentSpec
{
Replicas = 1,
Selector = new V1LabelSelector { MatchLabels = new Dictionary<string, string>(labels) },
Template = new V1PodTemplateSpec
{
Metadata = new V1ObjectMeta { Labels = new Dictionary<string, string>(labels) },
Spec = new V1PodSpec
{
Containers = new[]
{
new V1Container
{
Name = "k8scsharp-hpa",
Image = "nginx",
Resources = new V1ResourceRequirements
{
Requests = new Dictionary<string, ResourceQuantity>
{
{ "cpu", new ResourceQuantity("100m") },
{ "memory", new ResourceQuantity("128Mi") },
},
Limits = new Dictionary<string, ResourceQuantity>
{
{ "cpu", new ResourceQuantity("200m") },
{ "memory", new ResourceQuantity("256Mi") },
},
},
},
},
},
},
},
},
namespaceParameter).ConfigureAwait(false);
var hpa = new V2HorizontalPodAutoscaler
{
Metadata = new V1ObjectMeta { Name = hpaName },
Spec = new V2HorizontalPodAutoscalerSpec
{
MinReplicas = 1,
MaxReplicas = 3,
ScaleTargetRef = new V2CrossVersionObjectReference
{
ApiVersion = "apps/v1",
Kind = "Deployment",
Name = deploymentName,
},
Metrics = new List<V2MetricSpec>
{
new V2MetricSpec
{
Type = "Resource",
Resource = new V2ResourceMetricSource
{
Name = "cpu",
Target = new V2MetricTarget
{
Type = "Utilization",
AverageUtilization = 50,
},
},
},
},
},
};
await client.AutoscalingV2.CreateNamespacedHorizontalPodAutoscalerAsync(hpa, namespaceParameter).ConfigureAwait(false);
var hpaList = await client.AutoscalingV2.ListNamespacedHorizontalPodAutoscalerAsync(namespaceParameter).ConfigureAwait(false);
Assert.Contains(hpaList.Items, item => item.Metadata.Name == hpaName);
var created = await client.AutoscalingV2.ReadNamespacedHorizontalPodAutoscalerAsync(hpaName, namespaceParameter).ConfigureAwait(false);
Assert.Equal(1, created.Spec.MinReplicas);
created.Spec.MinReplicas = 2;
await client.AutoscalingV2.ReplaceNamespacedHorizontalPodAutoscalerAsync(created, hpaName, namespaceParameter).ConfigureAwait(false);
var updated = await client.AutoscalingV2.ReadNamespacedHorizontalPodAutoscalerAsync(hpaName, namespaceParameter).ConfigureAwait(false);
Assert.Equal(2, updated.Spec.MinReplicas);
await client.AutoscalingV2.DeleteNamespacedHorizontalPodAutoscalerAsync(hpaName, namespaceParameter, new V1DeleteOptions { PropagationPolicy = "Foreground" }).ConfigureAwait(false);
var retries = 10;
while (retries-- > 0)
{
hpaList = await client.AutoscalingV2.ListNamespacedHorizontalPodAutoscalerAsync(namespaceParameter).ConfigureAwait(false);
if (hpaList.Items.All(item => item.Metadata.Name != hpaName))
{
break;
}
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
}
Assert.DoesNotContain(hpaList.Items, item => item.Metadata.Name == hpaName);
}
finally
{
await CleanupAsync().ConfigureAwait(false);
}
}
public static IKubernetes CreateClient()
{
return new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig());

View File

@@ -7,7 +7,7 @@ using Xunit;
namespace k8s.tests;
public class BasicTests
public class SimpleTests
{
// TODO: fail to setup asp.net core 6 on net48
private class DummyHttpServer : System.IDisposable

View File

@@ -188,10 +188,18 @@ namespace k8s.Tests
}
else
{
#if NET9_0_OR_GREATER
serverCertificate = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(serverCertificateData), "");
#else
serverCertificate = new X509Certificate2(Convert.FromBase64String(serverCertificateData), "");
#endif
}
#if NET9_0_OR_GREATER
var clientCertificate = X509CertificateLoader.LoadCertificate(Convert.FromBase64String(clientCertificateData));
#else
var clientCertificate = new X509Certificate2(Convert.FromBase64String(clientCertificateData), "");
#endif
var clientCertificateValidationCalled = false;
@@ -294,10 +302,18 @@ namespace k8s.Tests
}
else
{
#if NET9_0_OR_GREATER
serverCertificate = X509CertificateLoader.LoadPkcs12(serverCertificateData, "");
#else
serverCertificate = new X509Certificate2(serverCertificateData, "");
#endif
}
#if NET9_0_OR_GREATER
var clientCertificate = X509CertificateLoader.LoadCertificate(clientCertificateData);
#else
var clientCertificate = new X509Certificate2(clientCertificateData, "");
#endif
var clientCertificateValidationCalled = false;

View File

@@ -85,8 +85,13 @@ namespace k8s.Tests
{
var certCollection = CertUtils.LoadPemFileCert("assets/ca-bundle.crt");
#if NET9_0_OR_GREATER
using var intermediateCert = X509CertificateLoader.LoadCertificateFromFile("assets/ca-bundle-intermediate.crt");
using var rootCert = X509CertificateLoader.LoadCertificateFromFile("assets/ca-bundle-root.crt");
#else
using var intermediateCert = new X509Certificate2("assets/ca-bundle-intermediate.crt");
using var rootCert = new X509Certificate2("assets/ca-bundle-root.crt");
#endif
Assert.Equal(2, certCollection.Count);

View File

@@ -68,7 +68,11 @@ namespace k8s.Tests
public void ValidCert()
{
var caCert = CertUtils.LoadPemFileCert("assets/ca.crt");
#if NET9_0_OR_GREATER
var testCert = X509CertificateLoader.LoadCertificateFromFile("assets/ca.crt");
#else
var testCert = new X509Certificate2("assets/ca.crt");
#endif
var chain = new X509Chain();
var errors = SslPolicyErrors.RemoteCertificateChainErrors;
@@ -81,7 +85,11 @@ namespace k8s.Tests
public void InvalidCert()
{
var caCert = CertUtils.LoadPemFileCert("assets/ca.crt");
#if NET9_0_OR_GREATER
var testCert = X509CertificateLoader.LoadCertificateFromFile("assets/ca2.crt");
#else
var testCert = new X509Certificate2("assets/ca2.crt");
#endif
var chain = new X509Chain();
var errors = SslPolicyErrors.RemoteCertificateChainErrors;
@@ -110,7 +118,11 @@ namespace k8s.Tests
public void InvalidBundleCert()
{
var caCert = CertUtils.LoadPemFileCert("assets/ca-bundle.crt");
#if NET9_0_OR_GREATER
var testCert = X509CertificateLoader.LoadCertificateFromFile("assets/ca2.crt");
#else
var testCert = new X509Certificate2("assets/ca2.crt");
#endif
var chain = new X509Chain();
var errors = SslPolicyErrors.RemoteCertificateChainErrors;

View File

@@ -10,13 +10,13 @@ namespace k8s.Tests
{
{
var v = 123;
IntstrIntOrString intorstr = v;
IntOrString intorstr = v;
Assert.Equal("123", KubernetesJson.Serialize(intorstr));
}
{
IntstrIntOrString intorstr = "12%";
IntOrString intorstr = "12%";
Assert.Equal("\"12%\"", KubernetesJson.Serialize(intorstr));
}
}
@@ -25,12 +25,12 @@ namespace k8s.Tests
public void Deserialize()
{
{
var v = KubernetesJson.Deserialize<IntstrIntOrString>("1234");
var v = KubernetesJson.Deserialize<IntOrString>("1234");
Assert.Equal("1234", v.Value);
}
{
var v = KubernetesJson.Deserialize<IntstrIntOrString>("\"12%\"");
var v = KubernetesJson.Deserialize<IntOrString>("\"12%\"");
Assert.Equal("12%", v.Value);
}
}

View File

@@ -1,5 +1,7 @@
using System;
using System.Text.Json;
using Xunit;
using k8s.Models;
namespace k8s.Tests;
@@ -116,4 +118,28 @@ public class KubernetesJsonTests
#endif
}
[Fact]
public void ReadWriteDatesJson()
{
var kManifest = """
{
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"creationTimestamp": "2025-09-03T05:15:53Z",
"name": "test-secret"
},
"type": "Opaque"
}
""";
var objFromJson = KubernetesJson.Deserialize<V1Secret>(kManifest);
var jsonFromObj = KubernetesJson.Serialize(objFromJson);
// Format Json
var jsonFromObj2 = JsonSerializer.Serialize(JsonSerializer.Deserialize<JsonElement>(jsonFromObj), new JsonSerializerOptions() { WriteIndented = true });
Assert.Equal(kManifest, jsonFromObj2);
}
}

View File

@@ -33,7 +33,7 @@ metadata:
}
#pragma warning disable CA1812 // Class is used for YAML deserialization tests
private class MyPod : V1Pod
private record MyPod : V1Pod
{
}
#pragma warning restore CA1812
@@ -531,7 +531,7 @@ spec:
var obj = new V1Service
{
Kind = "Service",
Metadata = new V1ObjectMeta(labels: labels, name: "test-svc"),
Metadata = new V1ObjectMeta { Name = "test-svc", Labels = labels },
ApiVersion = "v1",
Spec = new V1ServiceSpec
{
@@ -804,12 +804,12 @@ spec:
{
var kManifest = @"
apiVersion: v1
data:
username: YlhrdFlYQnc=
password: TXprMU1qZ2tkbVJuTjBwaQ==
kind: Secret
metadata:
name: test-secret
data:
username: bXktYXBw
password: Mzk1MjgkdmRnN0pi
";
var result = KubernetesYaml.Deserialize<V1Secret>(kManifest, true);
@@ -823,13 +823,8 @@ data:
var kManifest = """
apiVersion: v1
data:
username: bXktYXBw
tls2.crt: |
-----BEGIN CERTIFICATE-----
FAKE CERT
FAKE CERT
FAKE CERT
-----END CERTIFICATE-----
username: YlhrdFlYQnc=
password: TXprMU1qZ2tkbVJuTjBwaQ==
kind: Secret
metadata:
name: test-secret
@@ -841,6 +836,45 @@ metadata:
Assert.Equal(kManifest, yaml);
}
[Fact]
public void LoadConfigMap()
{
var kManifest = @"
apiVersion: v1
binaryData:
username: YlhrdFlYQnc=
data:
password: Mzk1MjgkdmRnN0pi
kind: ConfigMap
metadata:
name: test-configmap
";
var result = KubernetesYaml.Deserialize<V1ConfigMap>(kManifest, true);
Assert.Equal("bXktYXBw", Encoding.UTF8.GetString(result.BinaryData["username"]));
Assert.Equal("Mzk1MjgkdmRnN0pi", result.Data["password"]);
}
[Fact]
public void WriteConfigMap()
{
var kManifest = """
apiVersion: v1
binaryData:
username: YlhrdFlYQnc=
data:
password: Mzk1MjgkdmRnN0pi
kind: ConfigMap
metadata:
name: test-configmap
""";
var result = KubernetesYaml.Deserialize<V1ConfigMap>(kManifest, true);
var yaml = KubernetesYaml.Serialize(result);
Assert.Equal(kManifest, yaml);
}
[Fact]
public void DeserializeWithJsonPropertyName()
{
@@ -1136,5 +1170,148 @@ spec:
CultureInfo.CurrentCulture = old;
}
}
[Fact]
public void ReadWriteDatesYaml()
{
var kManifest = """
apiVersion: v1
kind: Secret
metadata:
creationTimestamp: "2025-09-03T05:15:53Z"
name: test-secret
type: Opaque
""";
var objFromYaml = KubernetesYaml.Deserialize<V1Secret>(kManifest, true);
var yamlFromObj = KubernetesYaml.Serialize(objFromYaml);
Assert.Equal(kManifest, yamlFromObj);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Build", "CA1812:'KubernetesYamlTests.RfcTime' is an internal class that is apparently never instantiated. If so, remove the code from the assembly. If this class is intended to contain only static members, make it 'static' (Module in Visual Basic). (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1812)", Justification = "json type")]
private class RfcTime
{
public DateTime Rfc3339 { get; set; }
public DateTime Rfc3339micro { get; set; }
public DateTime Rfc3339nano { get; set; }
public DateTime Rfc3339nanolenient1 { get; set; }
public DateTime Rfc3339nanolenient2 { get; set; }
public DateTime Rfc3339nanolenient3 { get; set; }
public DateTime Rfc3339nanolenient4 { get; set; }
public DateTime Rfc3339nanolenient5 { get; set; }
public DateTime Rfc3339nanolenient6 { get; set; }
public DateTime Rfc3339nanolenient7 { get; set; }
public DateTime Rfc3339nanolenient8 { get; set; }
public DateTime Rfc3339nanolenient9 { get; set; }
}
[Fact]
public void RFC3339()
{
/* go code to generate the json https://go.dev/play/p/VL95pugm6o8
const RFC3339Micro = "2006-01-02T15:04:05.000000Z07:00"
const RFC3339Nano = "2006-01-02T15:04:05.000000000Z07:00"
func main() {
t := time.Now()
type Time struct {
RFC3339 string `json:"rfc3339"`
RFC3339Micro string `json:"rfc3339micro"`
RFC3339Nano string `json:"rfc3339nano"`
RFC3339NanoLenient1 string `json:"rfc3339nanolenient1"`
RFC3339NanoLenient2 string `json:"rfc3339nanolenient2"`
RFC3339NanoLenient3 string `json:"rfc3339nanolenient3"`
RFC3339NanoLenient4 string `json:"rfc3339nanolenient4"`
RFC3339NanoLenient5 string `json:"rfc3339nanolenient5"`
RFC3339NanoLenient6 string `json:"rfc3339nanolenient6"`
RFC3339NanoLenient7 string `json:"rfc3339nanolenient7"`
RFC3339NanoLenient8 string `json:"rfc3339nanolenient8"`
RFC3339NanoLenient9 string `json:"rfc3339nanolenient9"`
}
t1 := Time{
RFC3339: t.Add(45 * time.Minute).Add(12 * time.Second).Add(123456789 * time.Nanosecond).Format(time.RFC3339),
RFC3339Micro: t.Add(45 * time.Minute).Add(12 * time.Second).Add(123456789 * time.Nanosecond).Format(RFC3339Micro),
RFC3339Nano: t.Add(24 * time.Minute).Add(56 * time.Second).Add(123456789 * time.Nanosecond).Format(RFC3339Nano),
RFC3339NanoLenient1: t.Add(100000000 * time.Nanosecond).Format(time.RFC3339Nano),
RFC3339NanoLenient2: t.Add(120000000 * time.Nanosecond).Format(time.RFC3339Nano),
RFC3339NanoLenient3: t.Add(123000000 * time.Nanosecond).Format(time.RFC3339Nano),
RFC3339NanoLenient4: t.Add(123400000 * time.Nanosecond).Format(time.RFC3339Nano),
RFC3339NanoLenient5: t.Add(123450000 * time.Nanosecond).Format(time.RFC3339Nano),
RFC3339NanoLenient6: t.Add(123456000 * time.Nanosecond).Format(time.RFC3339Nano),
RFC3339NanoLenient7: t.Add(123456700 * time.Nanosecond).Format(time.RFC3339Nano),
RFC3339NanoLenient8: t.Add(123456780 * time.Nanosecond).Format(time.RFC3339Nano),
RFC3339NanoLenient9: t.Add(123456789 * time.Nanosecond).Format(time.RFC3339Nano),
}
b, err := json.Marshal(t1)
if err != nil {
fmt.Println("error:", err)
}
fmt.Println(string(b))
}
*/
var yaml = """
rfc3339: '2009-11-10T23:45:12Z'
rfc3339micro: '2009-11-10T23:45:12.123456Z'
rfc3339nano: '2009-11-10T23:24:56.123456789Z'
rfc3339nanolenient1: '2009-11-10T23:00:00.1Z'
rfc3339nanolenient2: '2009-11-10T23:00:00.12Z'
rfc3339nanolenient3: '2009-11-10T23:00:00.123Z'
rfc3339nanolenient4: '2009-11-10T23:00:00.1234Z'
rfc3339nanolenient5: '2009-11-10T23:00:00.12345Z'
rfc3339nanolenient6: '2009-11-10T23:00:00.123456Z'
rfc3339nanolenient7: '2009-11-10T23:00:00.1234567Z'
rfc3339nanolenient8: '2009-11-10T23:00:00.12345678Z'
rfc3339nanolenient9: '2009-11-10T23:00:00.123456789Z'
""";
var t = KubernetesYaml.Deserialize<RfcTime>(yaml);
Assert.Equal(new DateTime(2009, 11, 10, 23, 45, 12, DateTimeKind.Utc), t.Rfc3339);
Assert.Equal(2009, t.Rfc3339micro.Year);
Assert.Equal(11, t.Rfc3339micro.Month);
Assert.Equal(10, t.Rfc3339micro.Day);
Assert.Equal(23, t.Rfc3339micro.Hour);
Assert.Equal(45, t.Rfc3339micro.Minute);
Assert.Equal(12, t.Rfc3339micro.Second);
Assert.Equal(123, t.Rfc3339micro.Millisecond);
Assert.Equal(2009, t.Rfc3339nano.Year);
Assert.Equal(11, t.Rfc3339nano.Month);
Assert.Equal(10, t.Rfc3339nano.Day);
Assert.Equal(23, t.Rfc3339nano.Hour);
Assert.Equal(24, t.Rfc3339nano.Minute);
Assert.Equal(56, t.Rfc3339nano.Second);
Assert.Equal(123, t.Rfc3339nano.Millisecond);
#if NET7_0_OR_GREATER
Assert.Equal(456, t.Rfc3339micro.Microsecond);
Assert.Equal(456, t.Rfc3339nano.Microsecond);
Assert.Equal(700, t.Rfc3339nano.Nanosecond);
Assert.Equal(100, t.Rfc3339nanolenient1.Millisecond);
Assert.Equal(120, t.Rfc3339nanolenient2.Millisecond);
Assert.Equal(123, t.Rfc3339nanolenient3.Millisecond);
Assert.Equal(400, t.Rfc3339nanolenient4.Microsecond);
Assert.Equal(450, t.Rfc3339nanolenient5.Microsecond);
Assert.Equal(456, t.Rfc3339nanolenient6.Microsecond);
Assert.Equal(456, t.Rfc3339nanolenient7.Microsecond);
Assert.Equal(456, t.Rfc3339nanolenient8.Microsecond);
Assert.Equal(456, t.Rfc3339nanolenient9.Microsecond);
Assert.Equal(700, t.Rfc3339nanolenient7.Nanosecond);
Assert.Equal(700, t.Rfc3339nanolenient8.Nanosecond);
Assert.Equal(700, t.Rfc3339nanolenient9.Nanosecond);
#endif
}
}
}

View File

@@ -54,24 +54,29 @@ namespace k8s.Tests
var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() });
// did not pass watch param
var listTask = client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true);
var onErrorCalled = false;
using (listTask.Watch<V1Pod, V1PodList>((type, item) => { }, e => { onErrorCalled = true; }))
using (var watcher = client.CoreV1.WatchListNamespacedPod(
"default",
onEvent: (type, item) => { },
onError: e => { onErrorCalled = true; }))
{
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(true); // delay for onerror to be called
}
Assert.True(onErrorCalled);
// server did not response line by line
await Assert.ThrowsAnyAsync<Exception>(() =>
{
return client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true);
using (var testWatcher = client.CoreV1.WatchListNamespacedPod(
"default"))
{
return Task.CompletedTask;
}
// this line did not throw
// listTask.Watch<Corev1Pod>((type, item) => { });
// using (var testWatcher = client.CoreV1.WatchListNamespacedPod("default", onEvent: (type, item) => { }))
}).ConfigureAwait(true);
}
}
@@ -93,8 +98,7 @@ namespace k8s.Tests
var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() });
var listTask = client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true);
using (listTask.Watch<V1Pod, V1PodList>((type, item) => { eventsReceived.Set(); }))
using (var watcher = client.CoreV1.WatchListNamespacedPod("default", onEvent: (type, item) => { eventsReceived.Set(); }))
{
// here watcher is ready to use, but http server has not responsed yet.
created.Set();
@@ -134,27 +138,26 @@ namespace k8s.Tests
{
var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() });
var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true);
var events = new HashSet<WatchEventType>();
var errors = 0;
var watcher = listTask.Watch<V1Pod, V1PodList>(
(type, item) =>
var watcher = client.CoreV1.WatchListNamespacedPod(
"default",
onEvent: (type, item) =>
{
testOutput.WriteLine($"Watcher received '{type}' event.");
events.Add(type);
eventsReceived.Signal();
},
error =>
onError: error =>
{
testOutput.WriteLine($"Watcher received '{error.GetType().FullName}' error.");
errors += 1;
eventsReceived.Signal();
},
connectionClosed.Set);
onClosed: connectionClosed.Set);
// wait server yields all events
await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true);
@@ -195,17 +198,16 @@ namespace k8s.Tests
{
var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() });
var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true);
var events = new HashSet<WatchEventType>();
var watcher = listTask.Watch<V1Pod, V1PodList>(
(type, item) =>
var watcher = client.CoreV1.WatchListNamespacedPod(
"default",
onEvent: (type, item) =>
{
events.Add(type);
eventsReceived.Signal();
},
error =>
onError: error =>
{
testOutput.WriteLine($"Watcher received '{error.GetType().FullName}' error.");
},
@@ -255,27 +257,26 @@ namespace k8s.Tests
{
var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() });
var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true);
var events = new HashSet<WatchEventType>();
var errors = 0;
var watcher = listTask.Watch<V1Pod, V1PodList>(
(type, item) =>
var watcher = client.CoreV1.WatchListNamespacedPod(
"default",
onEvent: (type, item) =>
{
testOutput.WriteLine($"Watcher received '{type}' event.");
events.Add(type);
eventsReceived.Signal();
},
error =>
onError: error =>
{
testOutput.WriteLine($"Watcher received '{error.GetType().FullName}' error.");
errors += 1;
eventsReceived.Signal();
},
waitForClosed.Set);
onClosed: waitForClosed.Set);
// wait server yields all events
await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true);
@@ -324,27 +325,26 @@ namespace k8s.Tests
{
var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() });
var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true);
var events = new HashSet<WatchEventType>();
var errors = 0;
var watcher = listTask.Watch<V1Pod, V1PodList>(
(type, item) =>
var watcher = client.CoreV1.WatchListNamespacedPod(
"default",
onEvent: (type, item) =>
{
testOutput.WriteLine($"Watcher received '{type}' event.");
events.Add(type);
eventsReceived.Signal();
},
error =>
onError: error =>
{
testOutput.WriteLine($"Watcher received '{error.GetType().FullName}' error.");
errors += 1;
eventsReceived.Signal();
},
connectionClosed.Set);
onClosed: connectionClosed.Set);
// wait server yields all events
await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true);
@@ -386,18 +386,17 @@ namespace k8s.Tests
{
var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() });
var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true);
waitForException.Set();
Watcher<V1Pod> watcher;
watcher = listTask.Watch<V1Pod, V1PodList>(
(type, item) => { },
e =>
watcher = client.CoreV1.WatchListNamespacedPod(
"default",
onEvent: (type, item) => { },
onError: e =>
{
exceptionCatched = e;
exceptionReceived.Set();
},
waitForClosed.Set);
onClosed: waitForClosed.Set);
// wait server down
await Task.WhenAny(exceptionReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true);
@@ -456,12 +455,11 @@ namespace k8s.Tests
Assert.False(handler1.Called);
Assert.False(handler2.Called);
var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true);
var events = new HashSet<WatchEventType>();
var watcher = listTask.Watch<V1Pod, V1PodList>(
(type, item) =>
var watcher = client.CoreV1.WatchListNamespacedPod(
"default",
onEvent: (type, item) =>
{
events.Add(type);
eventsReceived.Signal();
@@ -507,7 +505,9 @@ namespace k8s.Tests
var events = new HashSet<WatchEventType>();
var errors = 0;
var watcher = client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", fieldSelector: $"metadata.name=${"myPod"}", watch: true).Watch<V1Pod, V1PodList>(
var watcher = client.CoreV1.WatchListNamespacedPod(
"default",
fieldSelector: $"metadata.name=${"myPod"}",
onEvent:
(type, item) =>
{
@@ -568,7 +568,7 @@ namespace k8s.Tests
Host = server.Uri.ToString(),
HttpClientTimeout = TimeSpan.FromSeconds(5),
});
await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default").ConfigureAwait(true);
await client.CoreV1.ListNamespacedPodAsync("default").ConfigureAwait(true);
}).ConfigureAwait(true);
// cts
@@ -580,7 +580,7 @@ namespace k8s.Tests
{
Host = server.Uri.ToString(),
});
await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", cancellationToken: cts.Token).ConfigureAwait(true);
await client.CoreV1.ListNamespacedPodAsync("default", cancellationToken: cts.Token).ConfigureAwait(true);
}).ConfigureAwait(true);
}
@@ -608,7 +608,9 @@ namespace k8s.Tests
var events = new HashSet<WatchEventType>();
var errors = 0;
var watcher = client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", fieldSelector: $"metadata.name=${"myPod"}", watch: true).Watch<V1Pod, V1PodList>(
var watcher = client.CoreV1.WatchListNamespacedPod(
"default",
fieldSelector: $"metadata.name=${"myPod"}",
onEvent:
(type, item) =>
{
@@ -656,7 +658,6 @@ namespace k8s.Tests
httpContext.Response.StatusCode = 200;
await httpContext.Response.Body.FlushAsync().ConfigureAwait(true);
await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(true); // The default timeout is 100 seconds
return true;
}, resp: ""))
{
@@ -667,8 +668,10 @@ namespace k8s.Tests
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
{
await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true,
cancellationToken: cts.Token).ConfigureAwait(true);
using var watcher = client.CoreV1.WatchListNamespacedPod(
"default",
onEvent: (type, item) => { });
await Task.Delay(TimeSpan.FromSeconds(5), cts.Token).ConfigureAwait(true);
}).ConfigureAwait(true);
}
}
@@ -742,8 +745,288 @@ namespace k8s.Tests
new KubernetesClientConfiguration { Host = server.Uri.ToString() }, handler);
Assert.Null(handler.Version);
await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true);
using var watcher = client.CoreV1.WatchListNamespacedPod("default", onEvent: (type, item) => { });
Assert.Equal(HttpVersion.Version20, handler.Version);
await Task.CompletedTask.ConfigureAwait(true);
}
[Fact]
public async Task AsyncEnumerableWatchAllEvents()
{
var eventsReceived = new AsyncCountdownEvent(4);
var serverShutdown = new AsyncManualResetEvent();
var watchCompleted = new AsyncManualResetEvent();
using (var server = new MockKubeApiServer(testOutput, async httpContext =>
{
await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(true);
await WriteStreamLine(httpContext, MockDeletedStreamLine).ConfigureAwait(true);
await WriteStreamLine(httpContext, MockModifiedStreamLine).ConfigureAwait(true);
await WriteStreamLine(httpContext, MockErrorStreamLine).ConfigureAwait(true);
// make server alive, cannot set to int.max as of it would block response
await serverShutdown.WaitAsync().ConfigureAwait(true);
return false;
}))
{
var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() });
var events = new HashSet<WatchEventType>();
var errors = 0;
// Start async enumerable watch in background task
var watchTask = Task.Run(async () =>
{
try
{
await foreach (var (type, item) in client.CoreV1.WatchListNamespacedPodAsync("default").ConfigureAwait(false))
{
testOutput.WriteLine($"AsyncEnumerable Watcher received '{type}' event.");
events.Add(type);
eventsReceived.Signal();
// Break when we have all expected events
if (events.Count >= 4)
{
break;
}
}
}
catch (Exception ex)
{
testOutput.WriteLine($"AsyncEnumerable Watcher received exception: {ex.GetType().FullName}");
errors++;
eventsReceived.Signal();
}
finally
{
watchCompleted.Set();
}
});
// wait server yields all events
await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true);
Assert.True(
eventsReceived.CurrentCount == 0,
"Timed out waiting for all events / errors to be received.");
Assert.Contains(WatchEventType.Added, events);
Assert.Contains(WatchEventType.Deleted, events);
Assert.Contains(WatchEventType.Modified, events);
Assert.Contains(WatchEventType.Error, events);
Assert.Equal(0, errors);
serverShutdown.Set();
await Task.WhenAny(watchCompleted.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true);
Assert.True(watchCompleted.IsSet);
}
}
[Fact]
public async Task AsyncEnumerableWatchWithCancellation()
{
var eventsReceived = new AsyncCountdownEvent(2);
var serverShutdown = new AsyncManualResetEvent();
using (var server = new MockKubeApiServer(testOutput, async httpContext =>
{
await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(true);
await WriteStreamLine(httpContext, MockModifiedStreamLine).ConfigureAwait(true);
// Keep server alive
await serverShutdown.WaitAsync().ConfigureAwait(true);
return false;
}))
{
var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() });
var events = new HashSet<WatchEventType>();
var cts = new CancellationTokenSource();
var watchTask = Task.Run(async () =>
{
try
{
await foreach (var (type, item) in client.CoreV1.WatchListNamespacedPodAsync("default", cancellationToken: cts.Token).ConfigureAwait(false))
{
testOutput.WriteLine($"AsyncEnumerable Watcher received '{type}' event.");
events.Add(type);
eventsReceived.Signal();
}
}
catch (OperationCanceledException)
{
testOutput.WriteLine("AsyncEnumerable Watcher was cancelled as expected.");
}
});
// Wait for some events to be received
await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true);
Assert.True(
eventsReceived.CurrentCount == 0,
"Timed out waiting for events to be received.");
Assert.Contains(WatchEventType.Added, events);
Assert.Contains(WatchEventType.Modified, events);
// Cancel the watch
cts.Cancel();
// Wait for watch task to complete
await Task.WhenAny(watchTask, Task.Delay(TimeSpan.FromSeconds(5))).ConfigureAwait(true);
Assert.True(watchTask.IsCompletedSuccessfully || watchTask.IsCanceled);
serverShutdown.Set();
}
}
[Fact]
public async Task AsyncEnumerableWatchWithFieldSelector()
{
var eventsReceived = new AsyncCountdownEvent(3);
var serverShutdown = new AsyncManualResetEvent();
var watchCompleted = new AsyncManualResetEvent();
using (var server = new MockKubeApiServer(testOutput, async httpContext =>
{
await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(true);
await WriteStreamLine(httpContext, MockDeletedStreamLine).ConfigureAwait(true);
await WriteStreamLine(httpContext, MockModifiedStreamLine).ConfigureAwait(true);
await serverShutdown.WaitAsync().ConfigureAwait(true);
return false;
}))
{
var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() });
var events = new List<(WatchEventType, V1Pod)>();
var watchTask = Task.Run(async () =>
{
try
{
await foreach (var (type, item) in client.CoreV1.WatchListNamespacedPodAsync(
"default",
fieldSelector: $"metadata.name={"testPod"}").ConfigureAwait(false))
{
testOutput.WriteLine($"AsyncEnumerable Watcher received '{type}' event for pod '{item?.Metadata?.Name}'.");
events.Add((type, item));
eventsReceived.Signal();
if (events.Count >= 3)
{
break;
}
}
}
catch (Exception ex)
{
testOutput.WriteLine($"AsyncEnumerable Watcher received exception: {ex.GetType().FullName}");
}
finally
{
watchCompleted.Set();
}
});
// Wait for events
await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true);
Assert.True(
eventsReceived.CurrentCount == 0,
"Timed out waiting for all events to be received.");
Assert.Equal(3, events.Count);
Assert.Contains(events, e => e.Item1 == WatchEventType.Added);
Assert.Contains(events, e => e.Item1 == WatchEventType.Deleted);
Assert.Contains(events, e => e.Item1 == WatchEventType.Modified);
serverShutdown.Set();
await Task.WhenAny(watchCompleted.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true);
Assert.True(watchCompleted.IsSet);
}
}
[Fact]
public async Task AsyncEnumerableWatchErrorHandling()
{
var eventsReceived = new AsyncCountdownEvent(3);
var serverShutdown = new AsyncManualResetEvent();
var watchCompleted = new AsyncManualResetEvent();
var errorReceived = new AsyncManualResetEvent();
using (var server = new MockKubeApiServer(testOutput, async httpContext =>
{
await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse).ConfigureAwait(true);
await WriteStreamLine(httpContext, MockBadStreamLine).ConfigureAwait(true);
await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(true);
await serverShutdown.WaitAsync().ConfigureAwait(true);
return false;
}))
{
var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() });
var events = new List<(WatchEventType, V1Pod)>();
var errorCaught = false;
var watchTask = Task.Run(async () =>
{
try
{
await foreach (var (type, item) in client.CoreV1.WatchListNamespacedPodAsync(
"default",
onError: ex =>
{
testOutput.WriteLine($"AsyncEnumerable Watcher onError called: {ex.GetType().FullName}");
errorCaught = true;
errorReceived.Set();
eventsReceived.Signal();
}).ConfigureAwait(false))
{
testOutput.WriteLine($"AsyncEnumerable Watcher received '{type}' event.");
events.Add((type, item));
eventsReceived.Signal();
// Expect some valid events plus error handling
if (events.Count >= 2)
{
break;
}
}
}
catch (Exception ex)
{
testOutput.WriteLine($"AsyncEnumerable Watcher caught exception: {ex.GetType().FullName}");
}
finally
{
watchCompleted.Set();
}
});
// Wait for events and errors
await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true);
Assert.True(
eventsReceived.CurrentCount == 0,
"Timed out waiting for events and errors to be received.");
// Should have received at least one valid event and one error
Assert.True(events.Count >= 1, "Should have received at least one valid event");
Assert.True(errorCaught, "Should have caught parsing error");
Assert.True(errorReceived.IsSet, "Error callback should have been called");
serverShutdown.Set();
await Task.WhenAny(watchCompleted.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true);
Assert.True(watchCompleted.IsSet);
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
"version": "17.0",
"version": "18.0",
"publicReleaseRefSpec": [
"^refs/heads/master$",
"^refs/tags/v\\d+\\.\\d+\\.\\d+"