From 72a60220451543ac3f609a40824b5ad938119d1f Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Wed, 13 Dec 2023 10:36:40 -0800 Subject: [PATCH 01/19] init aot --- examples/aot/Program.cs | 16 ++++++++++++++++ examples/aot/aot.csproj | 10 ++++++++++ src/KubernetesClient/KubernetesClient.csproj | 5 ++--- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 examples/aot/Program.cs create mode 100644 examples/aot/aot.csproj diff --git a/examples/aot/Program.cs b/examples/aot/Program.cs new file mode 100644 index 000000000..d5125c0ff --- /dev/null +++ b/examples/aot/Program.cs @@ -0,0 +1,16 @@ +using k8s; + +var config = KubernetesClientConfiguration.BuildDefaultConfig(); +IKubernetes client = new Kubernetes(config); +Console.WriteLine("Starting Request!"); + +var list = client.CoreV1.ListNamespacedPod("default"); +foreach (var item in list.Items) +{ + Console.WriteLine(item.Metadata.Name); +} + +if (list.Items.Count == 0) +{ + Console.WriteLine("Empty!"); +} \ No newline at end of file diff --git a/examples/aot/aot.csproj b/examples/aot/aot.csproj new file mode 100644 index 000000000..1e6ee0744 --- /dev/null +++ b/examples/aot/aot.csproj @@ -0,0 +1,10 @@ + + + + Exe + enable + enable + true + + + diff --git a/src/KubernetesClient/KubernetesClient.csproj b/src/KubernetesClient/KubernetesClient.csproj index b3aaf808c..42bc14f42 100644 --- a/src/KubernetesClient/KubernetesClient.csproj +++ b/src/KubernetesClient/KubernetesClient.csproj @@ -3,9 +3,8 @@ net6.0;net7.0;net8.0 k8s - - + true + true true From b984a0cda08ee8b346e2c1ac2d0a4fe0503a05f9 Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Wed, 13 Dec 2023 10:44:13 -0800 Subject: [PATCH 02/19] fix ca2007 --- .../SimpleTests.cs | 2 +- tests/KubernetesClient.Tests/AuthTests.cs | 2 +- .../KubernetesClient.Tests/ByteBufferTests.cs | 8 +- .../KubernetesClientConfigurationTests.cs | 2 +- .../KubernetesExecTests.cs | 6 +- .../KubernetesMetricsTests.cs | 14 +- .../KubernetesYamlTests.cs | 12 +- tests/KubernetesClient.Tests/OidcAuthTests.cs | 6 +- tests/KubernetesClient.Tests/PodExecTests.cs | 20 +-- .../SerializationTests.cs | 8 +- .../StreamDemuxerTests.cs | 82 +++++----- .../TokenFileAuthTests.cs | 6 +- tests/KubernetesClient.Tests/WatchTests.cs | 140 +++++++++--------- 13 files changed, 154 insertions(+), 154 deletions(-) diff --git a/tests/KubernetesClient.Classic.Tests/SimpleTests.cs b/tests/KubernetesClient.Classic.Tests/SimpleTests.cs index b03c936df..762318f13 100644 --- a/tests/KubernetesClient.Classic.Tests/SimpleTests.cs +++ b/tests/KubernetesClient.Classic.Tests/SimpleTests.cs @@ -74,7 +74,7 @@ public async Task QueryPods() }); var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Addr }); - var pod = await client.CoreV1.ReadNamespacedPodAsync("pod", "default").ConfigureAwait(false); + var pod = await client.CoreV1.ReadNamespacedPodAsync("pod", "default").ConfigureAwait(true); Assert.Equal("pod0", pod.Metadata.Name); } diff --git a/tests/KubernetesClient.Tests/AuthTests.cs b/tests/KubernetesClient.Tests/AuthTests.cs index cd8777a73..3e9cf684d 100644 --- a/tests/KubernetesClient.Tests/AuthTests.cs +++ b/tests/KubernetesClient.Tests/AuthTests.cs @@ -541,7 +541,7 @@ private X509Certificate2 OpenCertificateStore(Stream stream) var store = new Pkcs12Store(); store.Load(stream, new char[] { }); - var keyAlias = store.Aliases.Cast().SingleOrDefault(a => store.IsKeyEntry(a)); + var keyAlias = store.Aliases.Cast().SingleOrDefault(store.IsKeyEntry); var key = (RsaPrivateCrtKeyParameters)store.GetKey(keyAlias).Key; var bouncyCertificate = store.GetCertificate(keyAlias).Certificate; diff --git a/tests/KubernetesClient.Tests/ByteBufferTests.cs b/tests/KubernetesClient.Tests/ByteBufferTests.cs index 223382bf6..9377b658d 100644 --- a/tests/KubernetesClient.Tests/ByteBufferTests.cs +++ b/tests/KubernetesClient.Tests/ByteBufferTests.cs @@ -255,7 +255,7 @@ public async Task ReadBlocksUntilDataAvailableTest() // Kick off a read operation var readTask = Task.Run(() => read = buffer.Read(readData, 0, readData.Length)); - await Task.Delay(250).ConfigureAwait(false); + await Task.Delay(250).ConfigureAwait(true); Assert.False(readTask.IsCompleted, "Read task completed before data was available."); // Write data to the buffer @@ -264,7 +264,7 @@ public async Task ReadBlocksUntilDataAvailableTest() await TaskAssert.Completed( readTask, TimeSpan.FromMilliseconds(1000), - "Timed out waiting for read task to complete.").ConfigureAwait(false); + "Timed out waiting for read task to complete.").ConfigureAwait(true); Assert.Equal(3, read); Assert.Equal(0xF0, readData[0]); @@ -411,10 +411,10 @@ public async Task ReadFirstTest() var output = new byte[buffer.Size + 1]; var readTask = Task.Run(() => buffer.Read(output, 0, output.Length)); - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(true); buffer.Write(data, 0, data.Length); - await readTask.ConfigureAwait(false); + await readTask.ConfigureAwait(true); } #if NETCOREAPP2_0 diff --git a/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs b/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs index 8f491a8ed..a1239c9e9 100644 --- a/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs +++ b/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs @@ -437,7 +437,7 @@ public async Task ContextWithClusterExtensions() { var path = Path.GetFullPath("assets/kubeconfig.cluster-extensions.yml"); - _ = await KubernetesClientConfiguration.BuildConfigFromConfigFileAsync(new FileInfo(path)).ConfigureAwait(false); + _ = await KubernetesClientConfiguration.BuildConfigFromConfigFileAsync(new FileInfo(path)).ConfigureAwait(true); } [Fact] diff --git a/tests/KubernetesClient.Tests/KubernetesExecTests.cs b/tests/KubernetesClient.Tests/KubernetesExecTests.cs index 1b7e39097..e0e0d5939 100644 --- a/tests/KubernetesClient.Tests/KubernetesExecTests.cs +++ b/tests/KubernetesClient.Tests/KubernetesExecTests.cs @@ -51,7 +51,7 @@ public async Task WebSocketNamespacedPodExecAsync() { { "X-My-Header", new List() { "myHeaderValue", "myHeaderValue2" } }, }, - cancellationToken: CancellationToken.None).ConfigureAwait(false); + cancellationToken: CancellationToken.None).ConfigureAwait(true); var expectedHeaders = new Dictionary() { @@ -91,7 +91,7 @@ public async Task WebSocketNamespacedPodPortForwardAsync() { { "X-My-Header", new List() { "myHeaderValue", "myHeaderValue2" } }, }, - cancellationToken: CancellationToken.None).ConfigureAwait(false); + cancellationToken: CancellationToken.None).ConfigureAwait(true); var expectedHeaders = new Dictionary() { @@ -137,7 +137,7 @@ public async Task WebSocketNamespacedPodAttachAsync() { { "X-My-Header", new List() { "myHeaderValue", "myHeaderValue2" } }, }, - cancellationToken: CancellationToken.None).ConfigureAwait(false); + cancellationToken: CancellationToken.None).ConfigureAwait(true); var expectedHeaders = new Dictionary() { diff --git a/tests/KubernetesClient.Tests/KubernetesMetricsTests.cs b/tests/KubernetesClient.Tests/KubernetesMetricsTests.cs index a616f26ef..96561a509 100644 --- a/tests/KubernetesClient.Tests/KubernetesMetricsTests.cs +++ b/tests/KubernetesClient.Tests/KubernetesMetricsTests.cs @@ -35,7 +35,7 @@ public async Task NodesMetrics() { var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }); - var nodesMetricsList = await client.GetKubernetesNodesMetricsAsync().ConfigureAwait(false); + var nodesMetricsList = await client.GetKubernetesNodesMetricsAsync().ConfigureAwait(true); Assert.Single(nodesMetricsList.Items); @@ -56,7 +56,7 @@ public async Task NodesMetricsOptionalProperty() var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }); // Should not throw with timespan optional property - var exception = await Record.ExceptionAsync(async () => await client.GetKubernetesNodesMetricsAsync().ConfigureAwait(false)).ConfigureAwait(false); + var exception = await Record.ExceptionAsync(client.GetKubernetesNodesMetricsAsync).ConfigureAwait(true); Assert.Null(exception); } @@ -69,7 +69,7 @@ public async Task PodsMetrics() { var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }); - var podsMetricsList = await client.GetKubernetesPodsMetricsAsync().ConfigureAwait(false); + var podsMetricsList = await client.GetKubernetesPodsMetricsAsync().ConfigureAwait(true); Assert.Single(podsMetricsList.Items); @@ -94,7 +94,7 @@ public async Task PodsMetricsOptionalProperty() var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }); // Should not throw with timespan optional property - var exception = await Record.ExceptionAsync(async () => await client.GetKubernetesPodsMetricsAsync().ConfigureAwait(false)).ConfigureAwait(false); + var exception = await Record.ExceptionAsync(client.GetKubernetesPodsMetricsAsync).ConfigureAwait(true); Assert.Null(exception); } @@ -107,7 +107,7 @@ public async Task PodsMetricsEmptyResponse() { var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }); - var podsMetricsList = await client.GetKubernetesPodsMetricsByNamespaceAsync("empty").ConfigureAwait(false); + var podsMetricsList = await client.GetKubernetesPodsMetricsByNamespaceAsync("empty").ConfigureAwait(true); Assert.Empty(podsMetricsList.Items); } @@ -122,7 +122,7 @@ public async Task PodsMetricsByNamespace() { var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }); - var podsMetricsList = await client.GetKubernetesPodsMetricsByNamespaceAsync(namespaceName).ConfigureAwait(false); + var podsMetricsList = await client.GetKubernetesPodsMetricsByNamespaceAsync(namespaceName).ConfigureAwait(true); Assert.Single(podsMetricsList.Items); @@ -147,7 +147,7 @@ public async Task PodsMetricsNonExistingNamespaceResponse() { var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }); - var podsMetricsList = await client.GetKubernetesPodsMetricsByNamespaceAsync("nonexisting").ConfigureAwait(false); + var podsMetricsList = await client.GetKubernetesPodsMetricsByNamespaceAsync("nonexisting").ConfigureAwait(true); Assert.Empty(podsMetricsList.Items); } diff --git a/tests/KubernetesClient.Tests/KubernetesYamlTests.cs b/tests/KubernetesClient.Tests/KubernetesYamlTests.cs index 9cd30e72a..56b2f7fbc 100644 --- a/tests/KubernetesClient.Tests/KubernetesYamlTests.cs +++ b/tests/KubernetesClient.Tests/KubernetesYamlTests.cs @@ -136,9 +136,9 @@ public async Task LoadAllFromFile() var tempFileName = Path.GetTempFileName(); try { - await File.WriteAllTextAsync(tempFileName, content).ConfigureAwait(false); + await File.WriteAllTextAsync(tempFileName, content).ConfigureAwait(true); - var objs = await KubernetesYaml.LoadAllFromFileAsync(tempFileName).ConfigureAwait(false); + var objs = await KubernetesYaml.LoadAllFromFileAsync(tempFileName).ConfigureAwait(true); Assert.Equal(2, objs.Count); Assert.IsType(objs[0]); Assert.IsType(objs[1]); @@ -175,9 +175,9 @@ public async Task LoadAllFromFileWithTypes() var tempFileName = Path.GetTempFileName(); try { - await File.WriteAllTextAsync(tempFileName, content).ConfigureAwait(false); + await File.WriteAllTextAsync(tempFileName, content).ConfigureAwait(true); - var objs = await KubernetesYaml.LoadAllFromFileAsync(tempFileName, types).ConfigureAwait(false); + var objs = await KubernetesYaml.LoadAllFromFileAsync(tempFileName, types).ConfigureAwait(true); Assert.Equal(2, objs.Count); Assert.IsType(objs[0]); Assert.IsType(objs[1]); @@ -309,9 +309,9 @@ public async Task LoadFromFile() var tempFileName = Path.GetTempFileName(); try { - await File.WriteAllTextAsync(tempFileName, content).ConfigureAwait(false); + await File.WriteAllTextAsync(tempFileName, content).ConfigureAwait(true); - var obj = await KubernetesYaml.LoadFromFileAsync(tempFileName).ConfigureAwait(false); + var obj = await KubernetesYaml.LoadFromFileAsync(tempFileName).ConfigureAwait(true); Assert.Equal("foo", obj.Metadata.Name); } finally diff --git a/tests/KubernetesClient.Tests/OidcAuthTests.cs b/tests/KubernetesClient.Tests/OidcAuthTests.cs index 8eccf4755..23eb7292d 100644 --- a/tests/KubernetesClient.Tests/OidcAuthTests.cs +++ b/tests/KubernetesClient.Tests/OidcAuthTests.cs @@ -21,7 +21,7 @@ public async Task TestOidcAuth() // use unexpired id token as bearer, do not attempt to refresh var auth = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, unexpiredIdToken, refreshToken); - var result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(false); + var result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(true); result.Scheme.Should().Be("Bearer"); result.Parameter.Should().Be(unexpiredIdToken); @@ -29,7 +29,7 @@ public async Task TestOidcAuth() { // attempt to refresh id token when expired auth = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, expiredIdToken, refreshToken); - result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(false); + result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(true); result.Scheme.Should().Be("Bearer"); result.Parameter.Should().Be(expiredIdToken); Assert.Fail("should not be here"); @@ -43,7 +43,7 @@ public async Task TestOidcAuth() { // attempt to refresh id token when null auth = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, null, refreshToken); - result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(false); + result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(true); result.Scheme.Should().Be("Bearer"); result.Parameter.Should().Be(expiredIdToken); Assert.Fail("should not be here"); diff --git a/tests/KubernetesClient.Tests/PodExecTests.cs b/tests/KubernetesClient.Tests/PodExecTests.cs index 067060c70..f4418fa6d 100644 --- a/tests/KubernetesClient.Tests/PodExecTests.cs +++ b/tests/KubernetesClient.Tests/PodExecTests.cs @@ -52,7 +52,7 @@ public async Task ExecDefaultContainerStdOut() TimeSpan.FromSeconds(5)); } - await Host.StartAsync(TestCancellation).ConfigureAwait(false); + await Host.StartAsync(TestCancellation).ConfigureAwait(true); using (Kubernetes client = CreateTestClient()) { @@ -67,7 +67,7 @@ public async Task ExecDefaultContainerStdOut() false, true, webSocketSubProtol: WebSocketProtocol.ChannelWebSocketProtocol, - cancellationToken: TestCancellation).ConfigureAwait(false); + cancellationToken: TestCancellation).ConfigureAwait(true); Assert.Equal( WebSocketProtocol.ChannelWebSocketProtocol, clientSocket.SubProtocol); // For WebSockets, the Kubernetes API defaults to the binary channel (v1) protocol. @@ -204,8 +204,8 @@ public async Task NamespacedPodExecAsyncActionNull() { await Assert.ThrowsAsync(() => client.NamespacedPodExecAsync( "pod-name", - "pod-namespace", "my-container", command, false, null, CancellationToken.None)) - .ConfigureAwait(false); + "pod-namespace", "my-container", command, false, null, CancellationToken.None)).ConfigureAwait(true) +; } } } @@ -229,8 +229,8 @@ public async Task NamespacedPodExecAsyncHttpExceptionWithStatus() { var ex = await Assert.ThrowsAsync(() => client.NamespacedPodExecAsync( "pod-name", - "pod-namespace", "my-container", command, false, handler, CancellationToken.None)) - .ConfigureAwait(false); + "pod-namespace", "my-container", command, false, handler, CancellationToken.None)).ConfigureAwait(true) +; Assert.Same(status, ex.Status); } } @@ -254,7 +254,7 @@ public async Task NamespacedPodExecAsyncHttpExceptionNoStatus() { var ex = await Assert.ThrowsAsync(() => client.NamespacedPodExecAsync("pod-name", "pod-namespace", "my-container", command, false, handler, - CancellationToken.None)).ConfigureAwait(false); + CancellationToken.None)).ConfigureAwait(true); Assert.Same(exception, ex); } } @@ -278,8 +278,8 @@ public async Task NamespacedPodExecAsyncGenericException() { var ex = await Assert.ThrowsAsync(() => client.NamespacedPodExecAsync( "pod-name", - "pod-namespace", "my-container", command, false, handler, CancellationToken.None)) - .ConfigureAwait(false); + "pod-namespace", "my-container", command, false, handler, CancellationToken.None)).ConfigureAwait(true) +; Assert.Same(exception, ex); } } @@ -330,7 +330,7 @@ public async Task NamespacedPodExecAsyncExitCodeNonZero() using (Kubernetes client = kubernetesMock.Object) { var exitCode = await client.NamespacedPodExecAsync("pod-name", "pod-namespace", "my-container", - command, false, handler, CancellationToken.None).ConfigureAwait(false); + command, false, handler, CancellationToken.None).ConfigureAwait(true); Assert.Equal(1, exitCode); } } diff --git a/tests/KubernetesClient.Tests/SerializationTests.cs b/tests/KubernetesClient.Tests/SerializationTests.cs index c6c95af3f..b11cba313 100644 --- a/tests/KubernetesClient.Tests/SerializationTests.cs +++ b/tests/KubernetesClient.Tests/SerializationTests.cs @@ -38,8 +38,8 @@ public async Task SerializeEnumUsingPascalCase() var customObject = Animals.Dog; - var result = await client.CustomObjects.CreateNamespacedCustomObjectWithHttpMessagesAsync(customObject, "TestGroup", "TestVersion", "TestNamespace", "TestPlural").ConfigureAwait(false); - var content = await result.Request.Content.ReadAsStringAsync().ConfigureAwait(false); + var result = await client.CustomObjects.CreateNamespacedCustomObjectWithHttpMessagesAsync(customObject, "TestGroup", "TestVersion", "TestNamespace", "TestPlural").ConfigureAwait(true); + var content = await result.Request.Content.ReadAsStringAsync().ConfigureAwait(true); // Assert that the client serializes using the default options. Assert.Equal(@"""Dog""", content); @@ -65,8 +65,8 @@ public async Task SerializeEnumUsingCamelCase() var customObject = Animals.Dog; - var result = await client.CustomObjects.CreateNamespacedCustomObjectWithHttpMessagesAsync(customObject, "TestGroup", "TestVersion", "TestNamespace", "TestPlural").ConfigureAwait(false); - var content = await result.Request.Content.ReadAsStringAsync().ConfigureAwait(false); + var result = await client.CustomObjects.CreateNamespacedCustomObjectWithHttpMessagesAsync(customObject, "TestGroup", "TestVersion", "TestNamespace", "TestPlural").ConfigureAwait(true); + var content = await result.Request.Content.ReadAsStringAsync().ConfigureAwait(true); // Assert that the client serializes using the specified options. Assert.Equal(@"""dog""", content); diff --git a/tests/KubernetesClient.Tests/StreamDemuxerTests.cs b/tests/KubernetesClient.Tests/StreamDemuxerTests.cs index 03d9981b5..365930446 100644 --- a/tests/KubernetesClient.Tests/StreamDemuxerTests.cs +++ b/tests/KubernetesClient.Tests/StreamDemuxerTests.cs @@ -29,7 +29,7 @@ public async Task SendDataRemoteCommand() // Send 100 bytes, expect 1 (channel index) + 100 (payload) = 101 bytes Assert.True( - await WaitForAsync(() => sentBuffer.Count == 101).ConfigureAwait(false), + await WaitForAsync(() => sentBuffer.Count == 101).ConfigureAwait(true), $"Demuxer error: expect to send 101 bytes, but actually send {sentBuffer.Count} bytes."); Assert.True(sentBuffer[0] == channelIndex, "The first sent byte is not channel index!"); Assert.True(sentBuffer[1] == 0xEF, "Incorrect payload!"); @@ -56,7 +56,7 @@ public async Task SendMultipleDataRemoteCommand() // Send 300 bytes in 2 messages, expect 1 (channel index) * 2 + 300 (payload) = 302 bytes Assert.True( - await WaitForAsync(() => sentBuffer.Count == 302).ConfigureAwait(false), + await WaitForAsync(() => sentBuffer.Count == 302).ConfigureAwait(true), $"Demuxer error: expect to send 302 bytes, but actually send {sentBuffer.Count} bytes."); Assert.True(sentBuffer[0] == channelIndex, "The first sent byte is not channel index!"); Assert.True(sentBuffer[1] == 0xEF, "The first part of payload incorrect!"); @@ -84,21 +84,21 @@ public async Task ReceiveDataRemoteCommand() { await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(100, channelIndex, 0xAA, false)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(200, channelIndex, 0xAB, false)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(300, channelIndex, 0xAC, false)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); - await WaitForAsync(() => receivedBuffer.Count == expectedCount).ConfigureAwait(false); - await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "normal", CancellationToken.None).ConfigureAwait(false); + await WaitForAsync(() => receivedBuffer.Count == expectedCount).ConfigureAwait(true); + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "normal", CancellationToken.None).ConfigureAwait(true); }); var buffer = new byte[50]; while (true) { - var cRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + var cRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(true); if (cRead == 0) { break; @@ -110,7 +110,7 @@ await ws.InvokeReceiveAsync( } } - await t.ConfigureAwait(false); + await t.ConfigureAwait(true); Assert.True( receivedBuffer.Count == expectedCount, @@ -144,21 +144,21 @@ public async Task ReceiveDataPortForward() { await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(100, channelIndex, 0xB1, true)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(200, channelIndex, 0xB2, false)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(300, channelIndex, 0xB3, false)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); - await WaitForAsync(() => receivedBuffer.Count == expectedCount).ConfigureAwait(false); - await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "normal", CancellationToken.None).ConfigureAwait(false); + await WaitForAsync(() => receivedBuffer.Count == expectedCount).ConfigureAwait(true); + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "normal", CancellationToken.None).ConfigureAwait(true); }); var buffer = new byte[50]; while (true) { - var cRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + var cRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(true); if (cRead == 0) { break; @@ -170,7 +170,7 @@ await ws.InvokeReceiveAsync( } } - await t.ConfigureAwait(false); + await t.ConfigureAwait(true); Assert.True( receivedBuffer.Count == expectedCount, @@ -204,21 +204,21 @@ public async Task ReceiveDataPortForwardOneByteMessage() { await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(2, channelIndex, 0xC1, true)), - WebSocketMessageType.Binary, false).ConfigureAwait(false); + WebSocketMessageType.Binary, false).ConfigureAwait(true); await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(100, channelIndex, 0xC2, false)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(300, channelIndex, 0xC3, false)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); - await WaitForAsync(() => receivedBuffer.Count == expectedCount).ConfigureAwait(false); - await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "normal", CancellationToken.None).ConfigureAwait(false); + await WaitForAsync(() => receivedBuffer.Count == expectedCount).ConfigureAwait(true); + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "normal", CancellationToken.None).ConfigureAwait(true); }); var buffer = new byte[50]; while (true) { - var cRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + var cRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(true); if (cRead == 0) { break; @@ -230,7 +230,7 @@ await ws.InvokeReceiveAsync( } } - await t.ConfigureAwait(false); + await t.ConfigureAwait(true); Assert.True( receivedBuffer.Count == expectedCount, @@ -268,24 +268,24 @@ public async Task ReceiveDataRemoteCommandMultipleStream() // Simulate WebSocket received remote data to multiple streams await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(100, channelIndex1, 0xD1, false)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(200, channelIndex2, 0xD2, false)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(300, channelIndex1, 0xD3, false)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); - await WaitForAsync(() => receivedBuffer1.Count == expectedCount1).ConfigureAwait(false); - await WaitForAsync(() => receivedBuffer2.Count == expectedCount2).ConfigureAwait(false); - await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "normal", CancellationToken.None).ConfigureAwait(false); + await WaitForAsync(() => receivedBuffer1.Count == expectedCount1).ConfigureAwait(true); + await WaitForAsync(() => receivedBuffer2.Count == expectedCount2).ConfigureAwait(true); + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "normal", CancellationToken.None).ConfigureAwait(true); }); var t2 = Task.Run(async () => { var buffer = new byte[50]; while (true) { - var cRead = await stream1.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + var cRead = await stream1.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(true); if (cRead == 0) { break; @@ -302,7 +302,7 @@ await ws.InvokeReceiveAsync( var buffer = new byte[50]; while (true) { - var cRead = await stream2.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + var cRead = await stream2.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(true); if (cRead == 0) { break; @@ -314,7 +314,7 @@ await ws.InvokeReceiveAsync( } } }); - await Task.WhenAll(t1, t2, t3).ConfigureAwait(false); + await Task.WhenAll(t1, t2, t3).ConfigureAwait(true); Assert.True( receivedBuffer1.Count == expectedCount1, @@ -359,24 +359,24 @@ public async Task ReceiveDataPortForwardMultipleStream() // Simulate WebSocket received remote data to multiple streams await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(100, channelIndex1, 0xE1, true)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(200, channelIndex2, 0xE2, true)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); await ws.InvokeReceiveAsync( new ArraySegment(GenerateRandomBuffer(300, channelIndex1, 0xE3, false)), - WebSocketMessageType.Binary, true).ConfigureAwait(false); + WebSocketMessageType.Binary, true).ConfigureAwait(true); - await WaitForAsync(() => receivedBuffer1.Count == expectedCount1).ConfigureAwait(false); - await WaitForAsync(() => receivedBuffer2.Count == expectedCount2).ConfigureAwait(false); - await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "normal", CancellationToken.None).ConfigureAwait(false); + await WaitForAsync(() => receivedBuffer1.Count == expectedCount1).ConfigureAwait(true); + await WaitForAsync(() => receivedBuffer2.Count == expectedCount2).ConfigureAwait(true); + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "normal", CancellationToken.None).ConfigureAwait(true); }); var t2 = Task.Run(async () => { var buffer = new byte[50]; while (true) { - var cRead = await stream1.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + var cRead = await stream1.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(true); if (cRead == 0) { break; @@ -393,7 +393,7 @@ await ws.InvokeReceiveAsync( var buffer = new byte[50]; while (true) { - var cRead = await stream2.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + var cRead = await stream2.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(true); if (cRead == 0) { break; @@ -405,7 +405,7 @@ await ws.InvokeReceiveAsync( } } }); - await Task.WhenAll(t1, t2, t3).ConfigureAwait(false); + await Task.WhenAll(t1, t2, t3).ConfigureAwait(true); Assert.True( receivedBuffer1.Count == expectedCount1, diff --git a/tests/KubernetesClient.Tests/TokenFileAuthTests.cs b/tests/KubernetesClient.Tests/TokenFileAuthTests.cs index 9d3be6bf8..5824917db 100644 --- a/tests/KubernetesClient.Tests/TokenFileAuthTests.cs +++ b/tests/KubernetesClient.Tests/TokenFileAuthTests.cs @@ -13,17 +13,17 @@ public class TokenFileAuthTests public async Task TestToken() { var auth = new TokenFileAuth("assets/token1"); - var result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(false); + var result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(true); result.Scheme.Should().Be("Bearer"); result.Parameter.Should().Be("token1"); auth.TokenFile = "assets/token2"; - result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(false); + result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(true); result.Scheme.Should().Be("Bearer"); result.Parameter.Should().Be("token1"); auth.TokenExpiresAt = DateTime.UtcNow.AddSeconds(-1); - result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(false); + result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(true); result.Scheme.Should().Be("Bearer"); result.Parameter.Should().Be("token2"); } diff --git a/tests/KubernetesClient.Tests/WatchTests.cs b/tests/KubernetesClient.Tests/WatchTests.cs index 3ddf6981c..f2e586208 100644 --- a/tests/KubernetesClient.Tests/WatchTests.cs +++ b/tests/KubernetesClient.Tests/WatchTests.cs @@ -59,7 +59,7 @@ public async Task CannotWatch() using (listTask.Watch((type, item) => { }, e => { onErrorCalled = true; })) { - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); // delay for onerror to be called + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(true); // delay for onerror to be called } Assert.True(onErrorCalled); @@ -72,7 +72,7 @@ await Assert.ThrowsAnyAsync(() => // this line did not throw // listTask.Watch((type, item) => { }); - }).ConfigureAwait(false); + }).ConfigureAwait(true); } } @@ -85,8 +85,8 @@ public async Task AsyncWatcher() using (var server = new MockKubeApiServer(testOutput, async httpContext => { // block until reponse watcher obj created - await created.WaitAsync().ConfigureAwait(false); - await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(false); + await created.WaitAsync().ConfigureAwait(true); + await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(true); return false; })) { @@ -98,7 +98,7 @@ public async Task AsyncWatcher() { // here watcher is ready to use, but http server has not responsed yet. created.Set(); - await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); } Assert.True(eventsReceived.IsSet); @@ -121,20 +121,20 @@ public async Task SurviveBadLine() httpContext.Response.StatusCode = (int)HttpStatusCode.OK; httpContext.Response.ContentLength = null; - await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockBadStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockBadStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockModifiedStreamLine).ConfigureAwait(false); + await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse).ConfigureAwait(true); + await WriteStreamLine(httpContext, MockBadStreamLine).ConfigureAwait(true); + await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(true); + await WriteStreamLine(httpContext, MockBadStreamLine).ConfigureAwait(true); + await WriteStreamLine(httpContext, MockModifiedStreamLine).ConfigureAwait(true); // make server alive, cannot set to int.max as of it would block response - await serverShutdown.WaitAsync().ConfigureAwait(false); + await serverShutdown.WaitAsync().ConfigureAwait(true); return false; })) { var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }); - var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(false); + var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true); var events = new HashSet(); var errors = 0; @@ -157,7 +157,7 @@ public async Task SurviveBadLine() connectionClosed.Set); // wait server yields all events - await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True( eventsReceived.CurrentCount == 0, @@ -173,7 +173,7 @@ public async Task SurviveBadLine() // Let the server know it can initiate a shut down. serverShutdown.Set(); - await Task.WhenAny(connectionClosed.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(connectionClosed.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True(connectionClosed.IsSet); } } @@ -187,15 +187,15 @@ public async Task DisposeWatch() using (var server = new MockKubeApiServer(testOutput, async httpContext => { - await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(false); - await serverShutdown.WaitAsync().ConfigureAwait(false); + await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(true); + await serverShutdown.WaitAsync().ConfigureAwait(true); return false; })) { var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }); - var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(false); + var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true); var events = new HashSet(); @@ -212,7 +212,7 @@ public async Task DisposeWatch() onClosed: connectionClosed.Set); // wait at least an event - await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True( eventsReceived.CurrentCount == 0, "Timed out waiting for events."); @@ -224,7 +224,7 @@ public async Task DisposeWatch() events.Clear(); - await Task.WhenAny(connectionClosed.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(connectionClosed.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.False(watcher.Watching); Assert.True(connectionClosed.IsSet); @@ -243,19 +243,19 @@ public async Task WatchAllEvents() using (var server = new MockKubeApiServer(testOutput, async httpContext => { - await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockDeletedStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockModifiedStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockErrorStreamLine).ConfigureAwait(false); + 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(false); + await serverShutdown.WaitAsync().ConfigureAwait(true); return false; })) { var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }); - var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(false); + var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true); var events = new HashSet(); var errors = 0; @@ -278,7 +278,7 @@ public async Task WatchAllEvents() waitForClosed.Set); // wait server yields all events - await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True( eventsReceived.CurrentCount == 0, @@ -295,7 +295,7 @@ public async Task WatchAllEvents() serverShutdown.Set(); - await Task.WhenAny(waitForClosed.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(waitForClosed.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True(waitForClosed.IsSet); Assert.False(watcher.Watching); } @@ -310,21 +310,21 @@ public async Task WatchEventsWithTimeout() using (var server = new MockKubeApiServer(testOutput, async httpContext => { - await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse).ConfigureAwait(false); - await Task.Delay(TimeSpan.FromSeconds(120)).ConfigureAwait(false); // The default timeout is 100 seconds - await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockDeletedStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockModifiedStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockErrorStreamLine).ConfigureAwait(false); + await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse).ConfigureAwait(true); + await Task.Delay(TimeSpan.FromSeconds(120)).ConfigureAwait(true); // The default timeout is 100 seconds + 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(false); + await serverShutdown.WaitAsync().ConfigureAwait(true); return false; })) { var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }); - var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(false); + var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true); var events = new HashSet(); var errors = 0; @@ -347,7 +347,7 @@ public async Task WatchEventsWithTimeout() connectionClosed.Set); // wait server yields all events - await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True( eventsReceived.CurrentCount == 0, @@ -364,7 +364,7 @@ public async Task WatchEventsWithTimeout() serverShutdown.Set(); - await Task.WhenAny(connectionClosed.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(connectionClosed.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True(connectionClosed.IsSet); } } @@ -379,14 +379,14 @@ public async Task WatchServerDisconnect() using (var server = new MockKubeApiServer(testOutput, async httpContext => { - await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse).ConfigureAwait(false); - await waitForException.WaitAsync().ConfigureAwait(false); + await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse).ConfigureAwait(true); + await waitForException.WaitAsync().ConfigureAwait(true); throw new IOException("server down"); })) { var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }); - var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(false); + var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true); waitForException.Set(); Watcher watcher; @@ -400,13 +400,13 @@ public async Task WatchServerDisconnect() waitForClosed.Set); // wait server down - await Task.WhenAny(exceptionReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(exceptionReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True( exceptionReceived.IsSet, "Timed out waiting for exception"); - await Task.WhenAny(waitForClosed.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(waitForClosed.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True(waitForClosed.IsSet); Assert.False(watcher.Watching); @@ -439,11 +439,11 @@ public async Task TestWatchWithHandlers() using (var server = new MockKubeApiServer(testOutput, async httpContext => { - await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(false); + await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse).ConfigureAwait(true); + await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(true); // make server alive, cannot set to int.max as of it would block response - await serverShutdown.WaitAsync().ConfigureAwait(false); + await serverShutdown.WaitAsync().ConfigureAwait(true); return false; })) { @@ -456,7 +456,7 @@ public async Task TestWatchWithHandlers() Assert.False(handler1.Called); Assert.False(handler2.Called); - var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(false); + var listTask = await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true); var events = new HashSet(); @@ -468,7 +468,7 @@ public async Task TestWatchWithHandlers() }); // wait server yields all events - await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True( eventsReceived.CurrentCount == 0, @@ -492,13 +492,13 @@ public async Task DirectWatchAllEvents() using (var server = new MockKubeApiServer(testOutput, async httpContext => { - await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockDeletedStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockModifiedStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockErrorStreamLine).ConfigureAwait(false); + 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(false); + await serverShutdown.WaitAsync().ConfigureAwait(true); return false; })) { @@ -527,7 +527,7 @@ public async Task DirectWatchAllEvents() onClosed: connectionClosed.Set); // wait server yields all events - await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True( eventsReceived.CurrentCount == 0, @@ -544,7 +544,7 @@ public async Task DirectWatchAllEvents() serverShutdown.Set(); - await Task.WhenAny(connectionClosed.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(connectionClosed.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True(connectionClosed.IsSet); } } @@ -554,8 +554,8 @@ public async Task EnsureTimeoutWorks() { using var server = new MockKubeApiServer(testOutput, async httpContext => { - await Task.Delay(TimeSpan.FromSeconds(120)).ConfigureAwait(false); // The default timeout is 100 seconds - await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(120)).ConfigureAwait(true); // The default timeout is 100 seconds + await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse).ConfigureAwait(true); return false; }); @@ -568,7 +568,7 @@ await Assert.ThrowsAsync(async () => Host = server.Uri.ToString(), HttpClientTimeout = TimeSpan.FromSeconds(5), }); - await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default").ConfigureAwait(false); + await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default").ConfigureAwait(true); }).ConfigureAwait(false); // cts @@ -580,7 +580,7 @@ await Assert.ThrowsAsync(async () => { Host = server.Uri.ToString(), }); - await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", cancellationToken: cts.Token).ConfigureAwait(false); + await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", cancellationToken: cts.Token).ConfigureAwait(true); }).ConfigureAwait(false); } @@ -592,14 +592,14 @@ public async Task DirectWatchEventsWithTimeout() using (var server = new MockKubeApiServer(testOutput, async httpContext => { - await Task.Delay(TimeSpan.FromSeconds(120)).ConfigureAwait(false); // The default timeout is 100 seconds - await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockDeletedStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockModifiedStreamLine).ConfigureAwait(false); - await WriteStreamLine(httpContext, MockErrorStreamLine).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(120)).ConfigureAwait(true); // The default timeout is 100 seconds + 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(false); + await serverShutdown.WaitAsync().ConfigureAwait(true); return false; })) { @@ -627,7 +627,7 @@ public async Task DirectWatchEventsWithTimeout() }); // wait server yields all events - await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(false); + await Task.WhenAny(eventsReceived.WaitAsync(), Task.Delay(TestTimeout)).ConfigureAwait(true); Assert.True( eventsReceived.CurrentCount == 0, @@ -654,8 +654,8 @@ public async Task WatchShouldCancelAfterRequested() using (var server = new MockKubeApiServer(testOutput, async httpContext => { httpContext.Response.StatusCode = 200; - await httpContext.Response.Body.FlushAsync().ConfigureAwait(false); - await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false); // The default timeout is 100 seconds + await httpContext.Response.Body.FlushAsync().ConfigureAwait(true); + await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(true); // The default timeout is 100 seconds return true; }, resp: "")) @@ -668,7 +668,7 @@ public async Task WatchShouldCancelAfterRequested() await Assert.ThrowsAnyAsync(async () => { await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true, - cancellationToken: cts.Token).ConfigureAwait(false); + cancellationToken: cts.Token).ConfigureAwait(true); }).ConfigureAwait(false); } } @@ -733,7 +733,7 @@ public async Task MustHttp2VersionSet() { var server = new MockKubeApiServer(testOutput, async httpContext => { - await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(false); + await WriteStreamLine(httpContext, MockAddedEventStreamLine).ConfigureAwait(true); return false; }); @@ -742,7 +742,7 @@ public async Task MustHttp2VersionSet() new KubernetesClientConfiguration { Host = server.Uri.ToString() }, handler); Assert.Null(handler.Version); - await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(false); + await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(true); Assert.Equal(HttpVersion.Version20, handler.Version); } } From cd2e86ef2d6ed9fc93ecdc24ddb0634422b169ce Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Wed, 13 Dec 2023 10:57:05 -0800 Subject: [PATCH 03/19] xUnit1031 --- .../LeaderElection/LeaderElectionTests.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs b/tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs index 22cb12412..387a8c9dd 100644 --- a/tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs +++ b/tests/KubernetesClient.Tests/LeaderElection/LeaderElectionTests.cs @@ -55,7 +55,7 @@ public void SimpleLeaderElection() }; var countdown = new CountdownEvent(2); - Task.Run(() => + Task.Run(async () => { var leaderElector = new LeaderElector(leaderElectionConfig); @@ -71,7 +71,7 @@ public void SimpleLeaderElection() countdown.Signal(); }; - leaderElector.RunUntilLeadershipLostAsync().Wait(); + await leaderElector.RunUntilLeadershipLostAsync().ConfigureAwait(true); }); countdown.Wait(TimeSpan.FromSeconds(10)); @@ -147,7 +147,7 @@ public void LeaderElection() var lockAStopLeading = new ManualResetEvent(false); var testLeaderElectionLatch = new CountdownEvent(4); - Task.Run(() => + Task.Run(async () => { var leaderElector = new LeaderElector(leaderElectionConfigA); @@ -164,13 +164,13 @@ public void LeaderElection() lockAStopLeading.Set(); }; - leaderElector.RunUntilLeadershipLostAsync().Wait(); + await leaderElector.RunUntilLeadershipLostAsync().ConfigureAwait(true); }); lockAStopLeading.WaitOne(TimeSpan.FromSeconds(3)); - Task.Run(() => + Task.Run(async () => { var leaderElector = new LeaderElector(leaderElectionConfigB); @@ -186,7 +186,7 @@ public void LeaderElection() testLeaderElectionLatch.Signal(); }; - leaderElector.RunUntilLeadershipLostAsync().Wait(); + await leaderElector.RunUntilLeadershipLostAsync().ConfigureAwait(true); }); testLeaderElectionLatch.Wait(TimeSpan.FromSeconds(15)); @@ -256,7 +256,7 @@ public void LeaderElectionWithRenewDeadline() }; var countdown = new CountdownEvent(2); - Task.Run(() => + Task.Run(async () => { var leaderElector = new LeaderElector(leaderElectionConfig); @@ -272,7 +272,7 @@ public void LeaderElectionWithRenewDeadline() countdown.Signal(); }; - leaderElector.RunUntilLeadershipLostAsync().Wait(); + await leaderElector.RunUntilLeadershipLostAsync().ConfigureAwait(true); }); countdown.Wait(TimeSpan.FromSeconds(15)); @@ -290,7 +290,7 @@ public void LeaderElectionWithRenewDeadline() } [Fact] - public void LeaderElectionThrowException() + public async Task LeaderElectionThrowException() { var l = new Mock(); l.Setup(obj => obj.GetAsync(CancellationToken.None)) @@ -305,7 +305,7 @@ public void LeaderElectionThrowException() try { - leaderElector.RunUntilLeadershipLostAsync().Wait(); + await leaderElector.RunUntilLeadershipLostAsync().ConfigureAwait(true); } catch (Exception e) { From c59e7fb015bab57ae2dc1814345d5c708513dbb3 Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Wed, 13 Dec 2023 10:57:18 -0800 Subject: [PATCH 04/19] fix ca2007 --- tests/KubernetesClient.Tests/PodExecTests.cs | 6 +++--- tests/KubernetesClient.Tests/WatchTests.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/KubernetesClient.Tests/PodExecTests.cs b/tests/KubernetesClient.Tests/PodExecTests.cs index f4418fa6d..5a6276e8d 100644 --- a/tests/KubernetesClient.Tests/PodExecTests.cs +++ b/tests/KubernetesClient.Tests/PodExecTests.cs @@ -82,10 +82,10 @@ public async Task ExecDefaultContainerStdOut() const int STDOUT = 1; const string expectedOutput = "This is text send to STDOUT."; - int bytesSent = await SendMultiplexed(serverSocket, STDOUT, expectedOutput).ConfigureAwait(false); + int bytesSent = await SendMultiplexed(serverSocket, STDOUT, expectedOutput).ConfigureAwait(true); testOutput.WriteLine($"Sent {bytesSent} bytes to server socket; receiving from client socket..."); - (string receivedText, byte streamIndex, int bytesReceived) = await ReceiveTextMultiplexed(clientSocket).ConfigureAwait(false); + (string receivedText, byte streamIndex, int bytesReceived) = await ReceiveTextMultiplexed(clientSocket).ConfigureAwait(true); testOutput.WriteLine( $"Received {bytesReceived} bytes from client socket ('{receivedText}', stream {streamIndex})."); @@ -94,7 +94,7 @@ public async Task ExecDefaultContainerStdOut() await Disconnect(clientSocket, serverSocket, WebSocketCloseStatus.NormalClosure, - "Normal Closure").ConfigureAwait(false); + "Normal Closure").ConfigureAwait(true); WebSocketTestAdapter.CompleteTest(); } diff --git a/tests/KubernetesClient.Tests/WatchTests.cs b/tests/KubernetesClient.Tests/WatchTests.cs index f2e586208..3b3a695d3 100644 --- a/tests/KubernetesClient.Tests/WatchTests.cs +++ b/tests/KubernetesClient.Tests/WatchTests.cs @@ -569,7 +569,7 @@ await Assert.ThrowsAsync(async () => HttpClientTimeout = TimeSpan.FromSeconds(5), }); await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default").ConfigureAwait(true); - }).ConfigureAwait(false); + }).ConfigureAwait(true); // cts await Assert.ThrowsAsync(async () => @@ -581,7 +581,7 @@ await Assert.ThrowsAsync(async () => Host = server.Uri.ToString(), }); await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", cancellationToken: cts.Token).ConfigureAwait(true); - }).ConfigureAwait(false); + }).ConfigureAwait(true); } [Fact] @@ -669,7 +669,7 @@ await Assert.ThrowsAnyAsync(async () => { await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true, cancellationToken: cts.Token).ConfigureAwait(true); - }).ConfigureAwait(false); + }).ConfigureAwait(true); } } From d1af6376eb97b60ba76c5871beb2292cb742ac24 Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Wed, 13 Dec 2023 10:57:26 -0800 Subject: [PATCH 05/19] fix ca2007 --- tests/E2E.Tests/MinikubeTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/E2E.Tests/MinikubeTests.cs b/tests/E2E.Tests/MinikubeTests.cs index 0ea8300bc..fc5136d5a 100644 --- a/tests/E2E.Tests/MinikubeTests.cs +++ b/tests/E2E.Tests/MinikubeTests.cs @@ -303,7 +303,7 @@ void DeleteEndpoints(string name) le.OnStartedLeading += () => leader1acq.Set(); le.OnStoppedLeading += () => leader1lose.Set(); - tasks.Add(le.RunAsync(cts.Token)); + tasks.Add(le.RunUntilLeadershipLostAsync(cts.Token)); } // wait 1 become leader @@ -325,7 +325,7 @@ void DeleteEndpoints(string name) leader2init.Set(); }; - tasks.Add(le.RunAsync()); + tasks.Add(le.RunUntilLeadershipLostAsync()); Assert.True(leader2init.WaitOne(TimeSpan.FromSeconds(30))); Assert.Equal("leader1", le.GetLeader()); From abfe7af8712980b85fc81ccb8ee630a0f1814ce8 Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Wed, 13 Dec 2023 10:57:39 -0800 Subject: [PATCH 06/19] remove deprecated ctor --- .../Autorest/HttpOperationException.cs | 13 ------------- src/KubernetesClient/KubernetesException.cs | 18 ------------------ 2 files changed, 31 deletions(-) diff --git a/src/KubernetesClient/Autorest/HttpOperationException.cs b/src/KubernetesClient/Autorest/HttpOperationException.cs index ac4117f34..e5da86864 100644 --- a/src/KubernetesClient/Autorest/HttpOperationException.cs +++ b/src/KubernetesClient/Autorest/HttpOperationException.cs @@ -1,14 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. -using System.Runtime.Serialization; - namespace k8s.Autorest { /// /// Exception thrown for an invalid response with custom error information. /// - [Serializable] public class HttpOperationException : Exception { /// @@ -51,15 +48,5 @@ public HttpOperationException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class. - /// - /// Serialization info. - /// Streaming context. - protected HttpOperationException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/src/KubernetesClient/KubernetesException.cs b/src/KubernetesClient/KubernetesException.cs index 0735b78f7..7d5ab6fb5 100644 --- a/src/KubernetesClient/KubernetesException.cs +++ b/src/KubernetesClient/KubernetesException.cs @@ -1,5 +1,3 @@ -using System.Runtime.Serialization; - namespace k8s { /// @@ -72,22 +70,6 @@ public KubernetesException(string message, Exception innerException) { } - /// - /// Initializes a new instance of the class with serialized data. - /// - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// - /// - /// The that contains contextual information - /// about the source or destination. - /// - protected KubernetesException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - /// /// Gets, when this exception was raised because of a Kubernetes status message, the underlying /// Kubernetes status message. From 872fbbac46aef0232e56effe101d8f109c75dcd2 Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Wed, 13 Dec 2023 11:01:28 -0800 Subject: [PATCH 07/19] fix xUnit1031 --- tests/KubernetesClient.Tests/KubernetesYamlTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/KubernetesClient.Tests/KubernetesYamlTests.cs b/tests/KubernetesClient.Tests/KubernetesYamlTests.cs index 56b2f7fbc..261e0819e 100644 --- a/tests/KubernetesClient.Tests/KubernetesYamlTests.cs +++ b/tests/KubernetesClient.Tests/KubernetesYamlTests.cs @@ -282,7 +282,7 @@ public void LoadPropertyNamedReadOnlyFromString() } [Fact] - public void LoadFromStream() + public async Task LoadFromStream() { var content = @"apiVersion: v1 kind: Pod @@ -292,7 +292,7 @@ public void LoadFromStream() using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); - var obj = KubernetesYaml.LoadFromStreamAsync(stream).Result; + var obj = await KubernetesYaml.LoadFromStreamAsync(stream).ConfigureAwait(true); Assert.Equal("foo", obj.Metadata.Name); } From d069afe1ba8f2bf288e92a46ddbc217d92d5eaf6 Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Wed, 13 Dec 2023 11:07:27 -0800 Subject: [PATCH 08/19] fix missing doc --- src/KubernetesClient/LeaderElection/LeaderElector.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/KubernetesClient/LeaderElection/LeaderElector.cs b/src/KubernetesClient/LeaderElection/LeaderElector.cs index dce925d5a..eb96b48a2 100644 --- a/src/KubernetesClient/LeaderElection/LeaderElector.cs +++ b/src/KubernetesClient/LeaderElection/LeaderElector.cs @@ -54,6 +54,7 @@ public string GetLeader() /// Will complete the returned Task and not retry to acquire leadership again after leadership is lost once. /// /// A token to cancel the operation. + /// A Task representing the asynchronous operation. public async Task RunUntilLeadershipLostAsync(CancellationToken cancellationToken = default) { await AcquireAsync(cancellationToken).ConfigureAwait(false); @@ -132,6 +133,7 @@ public async Task RunAndTryToHoldLeadershipForeverAsync(CancellationToken cancel /// /// /// A token to cancel the operation. + /// A Task representing the asynchronous operation. [Obsolete("Replaced by RunUntilLeadershipLostAsync to encode behavior in method name.")] public Task RunAsync(CancellationToken cancellationToken = default) { From 62974a0a033376325d86bff596a77076c3841200 Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Wed, 13 Dec 2023 11:07:36 -0800 Subject: [PATCH 09/19] fix missing dispose --- tests/KubernetesClient.Classic.Tests/SimpleTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/KubernetesClient.Classic.Tests/SimpleTests.cs b/tests/KubernetesClient.Classic.Tests/SimpleTests.cs index 762318f13..f0437defb 100644 --- a/tests/KubernetesClient.Classic.Tests/SimpleTests.cs +++ b/tests/KubernetesClient.Classic.Tests/SimpleTests.cs @@ -52,6 +52,9 @@ public void Dispose() { running = false; server.Stop(); +#if NET8_0_OR_GREATER + server.Dispose(); +#endif loop.Wait(); loop.Dispose(); } From b878e2aaa8a4091a73d6973dca2d49da7985314b Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Thu, 14 Dec 2023 14:26:15 -0800 Subject: [PATCH 10/19] wait for warnings fix --- .../KubeConfigModels/AuthProvider.cs | 1 + .../KubeConfigModels/Cluster.cs | 1 + .../KubeConfigModels/ClusterEndpoint.cs | 11 ++--- .../KubeConfigModels/Context.cs | 11 ++--- .../KubeConfigModels/ContextDetails.cs | 11 ++--- .../ExecCredentialResponse.cs | 3 ++ .../KubeConfigModels/ExternalExecution.cs | 1 + .../KubeConfigModels/K8SConfiguration.cs | 27 ++++++------ .../KubeConfigModels/NamedExtension.cs | 34 +++++++-------- .../KubeConfigModels/StaticContext.cs | 8 ++++ src/KubernetesClient/KubeConfigModels/User.cs | 1 + .../KubeConfigModels/UserCredentials.cs | 11 ++--- src/KubernetesClient/KubernetesClient.csproj | 1 + ...ubernetesClientConfiguration.ConfigFile.cs | 42 +++++++++---------- src/KubernetesClient/KubernetesJson.cs | 4 ++ src/KubernetesClient/KubernetesYaml.cs | 7 ++-- 16 files changed, 100 insertions(+), 74 deletions(-) create mode 100644 src/KubernetesClient/KubeConfigModels/StaticContext.cs diff --git a/src/KubernetesClient/KubeConfigModels/AuthProvider.cs b/src/KubernetesClient/KubeConfigModels/AuthProvider.cs index 4ec5d94e0..7c2f34bb1 100644 --- a/src/KubernetesClient/KubeConfigModels/AuthProvider.cs +++ b/src/KubernetesClient/KubeConfigModels/AuthProvider.cs @@ -5,6 +5,7 @@ namespace k8s.KubeConfigModels /// /// Contains information that describes identity information. This is use to tell the kubernetes cluster who you are. /// + [YamlSerializable] public class AuthProvider { /// diff --git a/src/KubernetesClient/KubeConfigModels/Cluster.cs b/src/KubernetesClient/KubeConfigModels/Cluster.cs index 4d0a81572..1c1a588a0 100644 --- a/src/KubernetesClient/KubeConfigModels/Cluster.cs +++ b/src/KubernetesClient/KubeConfigModels/Cluster.cs @@ -5,6 +5,7 @@ namespace k8s.KubeConfigModels /// /// Relates nicknames to cluster information. /// + [YamlSerializable] public class Cluster { /// diff --git a/src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs b/src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs index cdd791b4a..5ac94dfa7 100644 --- a/src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs +++ b/src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs @@ -5,6 +5,7 @@ namespace k8s.KubeConfigModels /// /// Contains information about how to communicate with a kubernetes cluster /// + [YamlSerializable] public class ClusterEndpoint { /// @@ -38,10 +39,10 @@ public class ClusterEndpoint [YamlMember(Alias = "insecure-skip-tls-verify", ApplyNamingConventions = false)] public bool SkipTlsVerify { get; set; } - /// - /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - /// - [YamlMember(Alias = "extensions")] - public IEnumerable Extensions { get; set; } + // /// + // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + // /// + // [YamlMember(Alias = "extensions")] + // public List Extensions { get; set; } } } diff --git a/src/KubernetesClient/KubeConfigModels/Context.cs b/src/KubernetesClient/KubeConfigModels/Context.cs index 931a9fa42..7834bbbdb 100644 --- a/src/KubernetesClient/KubeConfigModels/Context.cs +++ b/src/KubernetesClient/KubeConfigModels/Context.cs @@ -5,6 +5,7 @@ namespace k8s.KubeConfigModels /// /// Relates nicknames to context information. /// + [YamlSerializable] public class Context { /// @@ -19,11 +20,11 @@ public class Context [YamlMember(Alias = "name")] public string Name { get; set; } - /// - /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - /// - [YamlMember(Alias = "extensions")] - public IEnumerable Extensions { get; set; } + // /// + // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + // /// + // [YamlMember(Alias = "extensions")] + // public List Extensions { get; set; } [Obsolete("This property is not set by the YAML config. Use ContextDetails.Namespace instead.")] diff --git a/src/KubernetesClient/KubeConfigModels/ContextDetails.cs b/src/KubernetesClient/KubeConfigModels/ContextDetails.cs index 4bae5c3fd..5f0f526cd 100644 --- a/src/KubernetesClient/KubeConfigModels/ContextDetails.cs +++ b/src/KubernetesClient/KubeConfigModels/ContextDetails.cs @@ -6,6 +6,7 @@ namespace k8s.KubeConfigModels /// Represents a tuple of references to a cluster (how do I communicate with a kubernetes cluster), /// a user (how do I identify myself), and a namespace (what subset of resources do I want to work with) /// + [YamlSerializable] public class ContextDetails { /// @@ -26,10 +27,10 @@ public class ContextDetails [YamlMember(Alias = "namespace")] public string Namespace { get; set; } - /// - /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - /// - [YamlMember(Alias = "extensions")] - public IEnumerable Extensions { get; set; } + // /// + // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + // /// + // [YamlMember(Alias = "extensions")] + // public List Extensions { get; set; } } } diff --git a/src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs b/src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs index 6654ba3bd..5bac4af5e 100644 --- a/src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs +++ b/src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs @@ -1,5 +1,8 @@ +using YamlDotNet.Serialization; + namespace k8s.KubeConfigModels { + [YamlSerializable] public class ExecCredentialResponse { public class ExecStatus diff --git a/src/KubernetesClient/KubeConfigModels/ExternalExecution.cs b/src/KubernetesClient/KubeConfigModels/ExternalExecution.cs index c3be35f04..7e18f449e 100644 --- a/src/KubernetesClient/KubeConfigModels/ExternalExecution.cs +++ b/src/KubernetesClient/KubeConfigModels/ExternalExecution.cs @@ -2,6 +2,7 @@ namespace k8s.KubeConfigModels { + [YamlSerializable] public class ExternalExecution { [YamlMember(Alias = "apiVersion")] diff --git a/src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs b/src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs index 5308c209e..a0c3a4fef 100644 --- a/src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs +++ b/src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs @@ -10,13 +10,14 @@ namespace k8s.KubeConfigModels /// Should be kept in sync with https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go /// Should update MergeKubeConfig in KubernetesClientConfiguration.ConfigFile.cs if updated. /// + [YamlSerializable] public class K8SConfiguration { - /// - /// Gets or sets general information to be use for CLI interactions - /// - [YamlMember(Alias = "preferences")] - public IDictionary Preferences { get; set; } + // /// + // /// Gets or sets general information to be use for CLI interactions + // /// + // [YamlMember(Alias = "preferences")] + // public IDictionary Preferences { get; set; } [YamlMember(Alias = "apiVersion")] public string ApiVersion { get; set; } @@ -34,25 +35,25 @@ public class K8SConfiguration /// Gets or sets a map of referencable names to context configs. /// [YamlMember(Alias = "contexts")] - public IEnumerable Contexts { get; set; } = new Context[0]; + public List Contexts { get; set; } = new List(); /// /// Gets or sets a map of referencable names to cluster configs. /// [YamlMember(Alias = "clusters")] - public IEnumerable Clusters { get; set; } = new Cluster[0]; + public List Clusters { get; set; } = new List(); /// /// Gets or sets a map of referencable names to user configs /// [YamlMember(Alias = "users")] - public IEnumerable Users { get; set; } = new User[0]; + public List Users { get; set; } = new List(); - /// - /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - /// - [YamlMember(Alias = "extensions")] - public IEnumerable Extensions { get; set; } + // /// + // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + // /// + // [YamlMember(Alias = "extensions")] + // public List Extensions { get; set; } /// /// Gets or sets the name of the Kubernetes configuration file. This property is set only when the configuration diff --git a/src/KubernetesClient/KubeConfigModels/NamedExtension.cs b/src/KubernetesClient/KubeConfigModels/NamedExtension.cs index 676197975..07d6eda32 100644 --- a/src/KubernetesClient/KubeConfigModels/NamedExtension.cs +++ b/src/KubernetesClient/KubeConfigModels/NamedExtension.cs @@ -1,22 +1,22 @@ -using YamlDotNet.Serialization; namespace k8s.KubeConfigModels { - /// - /// relates nicknames to extension information - /// - public class NamedExtension - { - /// - /// Gets or sets the nickname for this extension. - /// - [YamlMember(Alias = "name")] - public string Name { get; set; } + // /// + // /// relates nicknames to extension information + // /// + // [YamlSerializable] + // public class NamedExtension + // { + // /// + // /// Gets or sets the nickname for this extension. + // /// + // [YamlMember(Alias = "name")] + // public string Name { get; set; } - /// - /// Get or sets the extension information. - /// - [YamlMember(Alias = "extension")] - public dynamic Extension { get; set; } - } + // /// + // /// Get or sets the extension information. + // /// + // [YamlMember(Alias = "extension")] + // public dynamic Extension { get; set; } + // } } diff --git a/src/KubernetesClient/KubeConfigModels/StaticContext.cs b/src/KubernetesClient/KubeConfigModels/StaticContext.cs new file mode 100644 index 000000000..ae9be922e --- /dev/null +++ b/src/KubernetesClient/KubeConfigModels/StaticContext.cs @@ -0,0 +1,8 @@ +using YamlDotNet.Serialization; + +namespace k8s.KubeConfigModels; + +[YamlStaticContext] +public partial class StaticContext : YamlDotNet.Serialization.StaticContext +{ +} \ No newline at end of file diff --git a/src/KubernetesClient/KubeConfigModels/User.cs b/src/KubernetesClient/KubeConfigModels/User.cs index b92d7c564..557f02256 100644 --- a/src/KubernetesClient/KubeConfigModels/User.cs +++ b/src/KubernetesClient/KubeConfigModels/User.cs @@ -5,6 +5,7 @@ namespace k8s.KubeConfigModels /// /// Relates nicknames to auth information. /// + [YamlSerializable] public class User { /// diff --git a/src/KubernetesClient/KubeConfigModels/UserCredentials.cs b/src/KubernetesClient/KubeConfigModels/UserCredentials.cs index b5dfc8a5a..dd4c4f139 100644 --- a/src/KubernetesClient/KubeConfigModels/UserCredentials.cs +++ b/src/KubernetesClient/KubeConfigModels/UserCredentials.cs @@ -5,6 +5,7 @@ namespace k8s.KubeConfigModels /// /// Contains information that describes identity information. This is use to tell the kubernetes cluster who you are. /// + [YamlSerializable] public class UserCredentials { /// @@ -73,11 +74,11 @@ public class UserCredentials [YamlMember(Alias = "auth-provider", ApplyNamingConventions = false)] public AuthProvider AuthProvider { get; set; } - /// - /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - /// - [YamlMember(Alias = "extensions")] - public IEnumerable Extensions { get; set; } + // /// + // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + // /// + // [YamlMember(Alias = "extensions")] + // public IEnumerable Extensions { get; set; } /// /// Gets or sets external command and its arguments to receive user credentials diff --git a/src/KubernetesClient/KubernetesClient.csproj b/src/KubernetesClient/KubernetesClient.csproj index 3a440dd16..0598aeaa2 100644 --- a/src/KubernetesClient/KubernetesClient.csproj +++ b/src/KubernetesClient/KubernetesClient.csproj @@ -14,6 +14,7 @@ + diff --git a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs index e74660bc9..da0284b72 100644 --- a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs +++ b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs @@ -459,16 +459,16 @@ public static Process CreateRunnableExternalProcess(ExternalExecution config, Ev throw new ArgumentNullException(nameof(config)); } - var execInfo = new Dictionary - { - { "apiVersion", config.ApiVersion }, - { "kind", "ExecCredentials" }, - { "spec", new Dictionary { { "interactive", Environment.UserInteractive } } }, - }; + // var execInfo = new Dictionary + // { + // { "apiVersion", config.ApiVersion }, + // { "kind", "ExecCredentials" }, + // { "spec", new Dictionary { { "interactive", Environment.UserInteractive } } }, + // }; var process = new Process(); - process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", JsonSerializer.Serialize(execInfo)); + process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", $"{{ \"apiVersion\":\"{config.ApiVersion}\",\"kind\":\"ExecCredentials\",\"spec\":{{ \"interactive\":{Environment.UserInteractive.ToString().ToLower()} }} }}"); if (config.EnvironmentVariables != null) { foreach (var configEnvironmentVariable in config.EnvironmentVariables) @@ -753,23 +753,23 @@ private static void MergeKubeConfig(K8SConfiguration basek8SConfig, K8SConfigura $"kubeconfig \"kind\" are different between {basek8SConfig.FileName} and {mergek8SConfig.FileName}"); } - if (mergek8SConfig.Preferences != null) - { - foreach (var preference in mergek8SConfig.Preferences) - { - if (basek8SConfig.Preferences?.ContainsKey(preference.Key) == false) - { - basek8SConfig.Preferences[preference.Key] = preference.Value; - } - } - } + // if (mergek8SConfig.Preferences != null) + // { + // foreach (var preference in mergek8SConfig.Preferences) + // { + // if (basek8SConfig.Preferences?.ContainsKey(preference.Key) == false) + // { + // basek8SConfig.Preferences[preference.Key] = preference.Value; + // } + // } + // } // Note, Clusters, Contexts, and Extensions are map-like in config despite being represented as a list here: // https://github.com/kubernetes/client-go/blob/ede92e0fe62deed512d9ceb8bf4186db9f3776ff/tools/clientcmd/api/types.go#L238 - basek8SConfig.Extensions = MergeLists(basek8SConfig.Extensions, mergek8SConfig.Extensions, (s) => s.Name); - basek8SConfig.Clusters = MergeLists(basek8SConfig.Clusters, mergek8SConfig.Clusters, (s) => s.Name); - basek8SConfig.Users = MergeLists(basek8SConfig.Users, mergek8SConfig.Users, (s) => s.Name); - basek8SConfig.Contexts = MergeLists(basek8SConfig.Contexts, mergek8SConfig.Contexts, (s) => s.Name); + // basek8SConfig.Extensions = MergeLists(basek8SConfig.Extensions, mergek8SConfig.Extensions, (s) => s.Name).ToList(); + basek8SConfig.Clusters = MergeLists(basek8SConfig.Clusters, mergek8SConfig.Clusters, (s) => s.Name).ToList(); + basek8SConfig.Users = MergeLists(basek8SConfig.Users, mergek8SConfig.Users, (s) => s.Name).ToList(); + basek8SConfig.Contexts = MergeLists(basek8SConfig.Contexts, mergek8SConfig.Contexts, (s) => s.Name).ToList(); } private static IEnumerable MergeLists(IEnumerable baseList, IEnumerable mergeList, diff --git a/src/KubernetesClient/KubernetesJson.cs b/src/KubernetesClient/KubernetesJson.cs index d0d4e9d13..2450b7d9d 100644 --- a/src/KubernetesClient/KubernetesJson.cs +++ b/src/KubernetesClient/KubernetesJson.cs @@ -78,6 +78,10 @@ static KubernetesJson() JsonSerializerOptions.Converters.Add(new KubernetesDateTimeOffsetConverter()); JsonSerializerOptions.Converters.Add(new V1Status.V1StatusObjectViewConverter()); JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + +#if NET8_0_OR_GREATER + JsonSerializerOptions.TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? new System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver() : SourceGenerationContext.Default; +#endif } /// diff --git a/src/KubernetesClient/KubernetesYaml.cs b/src/KubernetesClient/KubernetesYaml.cs index b40ed8d44..8d89874b2 100644 --- a/src/KubernetesClient/KubernetesYaml.cs +++ b/src/KubernetesClient/KubernetesYaml.cs @@ -12,14 +12,15 @@ namespace k8s /// public static class KubernetesYaml { - private static DeserializerBuilder CommonDeserializerBuilder => - new DeserializerBuilder() + private static StaticDeserializerBuilder CommonDeserializerBuilder => + new StaticDeserializerBuilder(new k8s.KubeConfigModels.StaticContext()) .WithNamingConvention(CamelCaseNamingConvention.Instance) .WithTypeConverter(new IntOrStringYamlConverter()) .WithTypeConverter(new ByteArrayStringYamlConverter()) .WithTypeConverter(new ResourceQuantityYamlConverter()) .WithAttemptingUnquotedStringTypeDeserialization() - .WithOverridesFromJsonPropertyAttributes(); + // .WithOverridesFromJsonPropertyAttributes() + ; private static readonly IDeserializer StrictDeserializer = CommonDeserializerBuilder From a7c740bec8f1359478c119793f7420886e5ad8bd Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Thu, 14 Dec 2023 15:21:36 -0800 Subject: [PATCH 11/19] fix space --- src/KubernetesClient/KubeConfigModels/AuthProvider.cs | 2 +- src/KubernetesClient/KubeConfigModels/Cluster.cs | 2 +- src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs | 2 +- src/KubernetesClient/KubeConfigModels/ContextDetails.cs | 2 +- src/KubernetesClient/KubeConfigModels/UserCredentials.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/KubernetesClient/KubeConfigModels/AuthProvider.cs b/src/KubernetesClient/KubeConfigModels/AuthProvider.cs index 7c2f34bb1..5bec9095e 100644 --- a/src/KubernetesClient/KubeConfigModels/AuthProvider.cs +++ b/src/KubernetesClient/KubeConfigModels/AuthProvider.cs @@ -5,7 +5,7 @@ namespace k8s.KubeConfigModels /// /// Contains information that describes identity information. This is use to tell the kubernetes cluster who you are. /// - [YamlSerializable] + [YamlSerializable] public class AuthProvider { /// diff --git a/src/KubernetesClient/KubeConfigModels/Cluster.cs b/src/KubernetesClient/KubeConfigModels/Cluster.cs index 1c1a588a0..80faf96a5 100644 --- a/src/KubernetesClient/KubeConfigModels/Cluster.cs +++ b/src/KubernetesClient/KubeConfigModels/Cluster.cs @@ -5,7 +5,7 @@ namespace k8s.KubeConfigModels /// /// Relates nicknames to cluster information. /// - [YamlSerializable] + [YamlSerializable] public class Cluster { /// diff --git a/src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs b/src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs index 5ac94dfa7..06f8e1016 100644 --- a/src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs +++ b/src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs @@ -5,7 +5,7 @@ namespace k8s.KubeConfigModels /// /// Contains information about how to communicate with a kubernetes cluster /// - [YamlSerializable] + [YamlSerializable] public class ClusterEndpoint { /// diff --git a/src/KubernetesClient/KubeConfigModels/ContextDetails.cs b/src/KubernetesClient/KubeConfigModels/ContextDetails.cs index 5f0f526cd..6cbfd0073 100644 --- a/src/KubernetesClient/KubeConfigModels/ContextDetails.cs +++ b/src/KubernetesClient/KubeConfigModels/ContextDetails.cs @@ -6,7 +6,7 @@ namespace k8s.KubeConfigModels /// Represents a tuple of references to a cluster (how do I communicate with a kubernetes cluster), /// a user (how do I identify myself), and a namespace (what subset of resources do I want to work with) /// - [YamlSerializable] + [YamlSerializable] public class ContextDetails { /// diff --git a/src/KubernetesClient/KubeConfigModels/UserCredentials.cs b/src/KubernetesClient/KubeConfigModels/UserCredentials.cs index dd4c4f139..8ef46fc95 100644 --- a/src/KubernetesClient/KubeConfigModels/UserCredentials.cs +++ b/src/KubernetesClient/KubeConfigModels/UserCredentials.cs @@ -5,7 +5,7 @@ namespace k8s.KubeConfigModels /// /// Contains information that describes identity information. This is use to tell the kubernetes cluster who you are. /// - [YamlSerializable] + [YamlSerializable] public class UserCredentials { /// From 8a0900353c7ead353cd18e939a27aefd4685b57d Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Sat, 13 Jan 2024 03:12:32 -0800 Subject: [PATCH 12/19] move aot code to dedicated proj --- examples/Directory.Build.targets | 2 +- examples/aot/aot.csproj | 5 +- kubernetes-client.ruleset | 2 +- src/KubernetesClient.Aot/Global.cs | 10 + .../KubeConfigModels/AuthProvider.cs | 23 + .../KubeConfigModels/Cluster.cs | 23 + .../KubeConfigModels/ClusterEndpoint.cs | 48 ++ .../KubeConfigModels/Context.cs | 23 + .../KubeConfigModels/ContextDetails.cs | 36 + .../ExecCredentialResponse.cs | 31 + .../KubeConfigModels/ExternalExecution.cs | 42 + .../KubeConfigModels/K8SConfiguration.cs | 65 ++ .../KubeConfigModels/StaticContext.cs | 0 .../KubeConfigModels/User.cs | 23 + .../KubeConfigModels/UserCredentials.cs | 89 ++ .../KubernetesClient.Aot.csproj | 114 +++ ...ubernetesClientConfiguration.ConfigFile.cs | 760 ++++++++++++++++++ src/KubernetesClient.Aot/KubernetesJson.cs | 118 +++ src/KubernetesClient.Aot/KubernetesYaml.cs | 255 ++++++ .../KubeConfigModels/AuthProvider.cs | 1 - .../KubeConfigModels/Cluster.cs | 1 - .../KubeConfigModels/ClusterEndpoint.cs | 11 +- .../KubeConfigModels/Context.cs | 11 +- .../KubeConfigModels/ContextDetails.cs | 11 +- .../ExecCredentialResponse.cs | 3 - .../KubeConfigModels/ExternalExecution.cs | 1 - .../KubeConfigModels/K8SConfiguration.cs | 27 +- .../KubeConfigModels/NamedExtension.cs | 34 +- src/KubernetesClient/KubeConfigModels/User.cs | 1 - .../KubeConfigModels/UserCredentials.cs | 11 +- src/KubernetesClient/KubernetesClient.csproj | 6 +- ...ubernetesClientConfiguration.ConfigFile.cs | 42 +- src/KubernetesClient/KubernetesJson.cs | 4 - src/KubernetesClient/KubernetesYaml.cs | 7 +- 34 files changed, 1742 insertions(+), 98 deletions(-) create mode 100644 src/KubernetesClient.Aot/Global.cs create mode 100644 src/KubernetesClient.Aot/KubeConfigModels/AuthProvider.cs create mode 100644 src/KubernetesClient.Aot/KubeConfigModels/Cluster.cs create mode 100644 src/KubernetesClient.Aot/KubeConfigModels/ClusterEndpoint.cs create mode 100644 src/KubernetesClient.Aot/KubeConfigModels/Context.cs create mode 100644 src/KubernetesClient.Aot/KubeConfigModels/ContextDetails.cs create mode 100644 src/KubernetesClient.Aot/KubeConfigModels/ExecCredentialResponse.cs create mode 100644 src/KubernetesClient.Aot/KubeConfigModels/ExternalExecution.cs create mode 100644 src/KubernetesClient.Aot/KubeConfigModels/K8SConfiguration.cs rename src/{KubernetesClient => KubernetesClient.Aot}/KubeConfigModels/StaticContext.cs (100%) create mode 100644 src/KubernetesClient.Aot/KubeConfigModels/User.cs create mode 100644 src/KubernetesClient.Aot/KubeConfigModels/UserCredentials.cs create mode 100644 src/KubernetesClient.Aot/KubernetesClient.Aot.csproj create mode 100644 src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs create mode 100644 src/KubernetesClient.Aot/KubernetesJson.cs create mode 100644 src/KubernetesClient.Aot/KubernetesYaml.cs diff --git a/examples/Directory.Build.targets b/examples/Directory.Build.targets index 3b7810177..65c630b6d 100644 --- a/examples/Directory.Build.targets +++ b/examples/Directory.Build.targets @@ -1,5 +1,5 @@ - + diff --git a/examples/aot/aot.csproj b/examples/aot/aot.csproj index 1e6ee0744..28741906d 100644 --- a/examples/aot/aot.csproj +++ b/examples/aot/aot.csproj @@ -1,10 +1,11 @@ - Exe enable enable true - + + + diff --git a/kubernetes-client.ruleset b/kubernetes-client.ruleset index 8baea16b9..151975f56 100644 --- a/kubernetes-client.ruleset +++ b/kubernetes-client.ruleset @@ -268,7 +268,7 @@ - + diff --git a/src/KubernetesClient.Aot/Global.cs b/src/KubernetesClient.Aot/Global.cs new file mode 100644 index 000000000..2b5a4ae8e --- /dev/null +++ b/src/KubernetesClient.Aot/Global.cs @@ -0,0 +1,10 @@ +global using k8s.Autorest; +global using k8s.Models; +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/src/KubernetesClient.Aot/KubeConfigModels/AuthProvider.cs b/src/KubernetesClient.Aot/KubeConfigModels/AuthProvider.cs new file mode 100644 index 000000000..5bec9095e --- /dev/null +++ b/src/KubernetesClient.Aot/KubeConfigModels/AuthProvider.cs @@ -0,0 +1,23 @@ +using YamlDotNet.Serialization; + +namespace k8s.KubeConfigModels +{ + /// + /// Contains information that describes identity information. This is use to tell the kubernetes cluster who you are. + /// + [YamlSerializable] + public class AuthProvider + { + /// + /// Gets or sets the nickname for this auth provider. + /// + [YamlMember(Alias = "name")] + public string Name { get; set; } + + /// + /// Gets or sets the configuration for this auth provider + /// + [YamlMember(Alias = "config")] + public Dictionary Config { get; set; } + } +} diff --git a/src/KubernetesClient.Aot/KubeConfigModels/Cluster.cs b/src/KubernetesClient.Aot/KubeConfigModels/Cluster.cs new file mode 100644 index 000000000..80faf96a5 --- /dev/null +++ b/src/KubernetesClient.Aot/KubeConfigModels/Cluster.cs @@ -0,0 +1,23 @@ +using YamlDotNet.Serialization; + +namespace k8s.KubeConfigModels +{ + /// + /// Relates nicknames to cluster information. + /// + [YamlSerializable] + public class Cluster + { + /// + /// Gets or sets the cluster information. + /// + [YamlMember(Alias = "cluster")] + public ClusterEndpoint ClusterEndpoint { get; set; } + + /// + /// Gets or sets the nickname for this Cluster. + /// + [YamlMember(Alias = "name")] + public string Name { get; set; } + } +} diff --git a/src/KubernetesClient.Aot/KubeConfigModels/ClusterEndpoint.cs b/src/KubernetesClient.Aot/KubeConfigModels/ClusterEndpoint.cs new file mode 100644 index 000000000..06f8e1016 --- /dev/null +++ b/src/KubernetesClient.Aot/KubeConfigModels/ClusterEndpoint.cs @@ -0,0 +1,48 @@ +using YamlDotNet.Serialization; + +namespace k8s.KubeConfigModels +{ + /// + /// Contains information about how to communicate with a kubernetes cluster + /// + [YamlSerializable] + public class ClusterEndpoint + { + /// + /// Gets or sets the path to a cert file for the certificate authority. + /// + [YamlMember(Alias = "certificate-authority", ApplyNamingConventions = false)] + public string CertificateAuthority { get; set; } + + /// + /// Gets or sets =PEM-encoded certificate authority certificates. Overrides . + /// + [YamlMember(Alias = "certificate-authority-data", ApplyNamingConventions = false)] + public string CertificateAuthorityData { get; set; } + + /// + /// Gets or sets the address of the kubernetes cluster (https://hostname:port). + /// + [YamlMember(Alias = "server")] + public string Server { get; set; } + + /// + /// Gets or sets a value to override the TLS server name. + /// + [YamlMember(Alias = "tls-server-name", ApplyNamingConventions = false)] + public string TlsServerName { get; set; } + + /// + /// Gets or sets a value indicating whether to skip the validity check for the server's certificate. + /// This will make your HTTPS connections insecure. + /// + [YamlMember(Alias = "insecure-skip-tls-verify", ApplyNamingConventions = false)] + public bool SkipTlsVerify { get; set; } + + // /// + // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + // /// + // [YamlMember(Alias = "extensions")] + // public List Extensions { get; set; } + } +} diff --git a/src/KubernetesClient.Aot/KubeConfigModels/Context.cs b/src/KubernetesClient.Aot/KubeConfigModels/Context.cs new file mode 100644 index 000000000..65241315f --- /dev/null +++ b/src/KubernetesClient.Aot/KubeConfigModels/Context.cs @@ -0,0 +1,23 @@ +using YamlDotNet.Serialization; + +namespace k8s.KubeConfigModels +{ + /// + /// Relates nicknames to context information. + /// + [YamlSerializable] + public class Context + { + /// + /// Gets or sets the context information. + /// + [YamlMember(Alias = "context")] + public ContextDetails ContextDetails { get; set; } + + /// + /// Gets or sets the nickname for this context. + /// + [YamlMember(Alias = "name")] + public string Name { get; set; } + } +} diff --git a/src/KubernetesClient.Aot/KubeConfigModels/ContextDetails.cs b/src/KubernetesClient.Aot/KubeConfigModels/ContextDetails.cs new file mode 100644 index 000000000..6cbfd0073 --- /dev/null +++ b/src/KubernetesClient.Aot/KubeConfigModels/ContextDetails.cs @@ -0,0 +1,36 @@ +using YamlDotNet.Serialization; + +namespace k8s.KubeConfigModels +{ + /// + /// Represents a tuple of references to a cluster (how do I communicate with a kubernetes cluster), + /// a user (how do I identify myself), and a namespace (what subset of resources do I want to work with) + /// + [YamlSerializable] + public class ContextDetails + { + /// + /// Gets or sets the name of the cluster for this context. + /// + [YamlMember(Alias = "cluster")] + public string Cluster { get; set; } + + /// + /// Gets or sets the name of the user for this context. + /// + [YamlMember(Alias = "user")] + public string User { get; set; } + + /// + /// /Gets or sets the default namespace to use on unspecified requests. + /// + [YamlMember(Alias = "namespace")] + public string Namespace { get; set; } + + // /// + // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + // /// + // [YamlMember(Alias = "extensions")] + // public List Extensions { get; set; } + } +} diff --git a/src/KubernetesClient.Aot/KubeConfigModels/ExecCredentialResponse.cs b/src/KubernetesClient.Aot/KubeConfigModels/ExecCredentialResponse.cs new file mode 100644 index 000000000..5bac4af5e --- /dev/null +++ b/src/KubernetesClient.Aot/KubeConfigModels/ExecCredentialResponse.cs @@ -0,0 +1,31 @@ +using YamlDotNet.Serialization; + +namespace k8s.KubeConfigModels +{ + [YamlSerializable] + public class ExecCredentialResponse + { + public class ExecStatus + { +#nullable enable + public DateTime? ExpirationTimestamp { get; set; } + public string? Token { get; set; } + public string? ClientCertificateData { get; set; } + public string? ClientKeyData { get; set; } +#nullable disable + + public bool IsValid() + { + return !string.IsNullOrEmpty(Token) || + (!string.IsNullOrEmpty(ClientCertificateData) && !string.IsNullOrEmpty(ClientKeyData)); + } + } + + [JsonPropertyName("apiVersion")] + public string ApiVersion { get; set; } + [JsonPropertyName("kind")] + public string Kind { get; set; } + [JsonPropertyName("status")] + public ExecStatus Status { get; set; } + } +} diff --git a/src/KubernetesClient.Aot/KubeConfigModels/ExternalExecution.cs b/src/KubernetesClient.Aot/KubeConfigModels/ExternalExecution.cs new file mode 100644 index 000000000..7e18f449e --- /dev/null +++ b/src/KubernetesClient.Aot/KubeConfigModels/ExternalExecution.cs @@ -0,0 +1,42 @@ +using YamlDotNet.Serialization; + +namespace k8s.KubeConfigModels +{ + [YamlSerializable] + public class ExternalExecution + { + [YamlMember(Alias = "apiVersion")] + public string ApiVersion { get; set; } + + /// + /// The command to execute. Required. + /// + [YamlMember(Alias = "command")] + public string Command { get; set; } + + /// + /// Environment variables to set when executing the plugin. Optional. + /// + [YamlMember(Alias = "env")] + public IList> EnvironmentVariables { get; set; } + + /// + /// Arguments to pass when executing the plugin. Optional. + /// + [YamlMember(Alias = "args")] + public IList Arguments { get; set; } + + /// + /// Text shown to the user when the executable doesn't seem to be present. Optional. + /// + [YamlMember(Alias = "installHint")] + public string InstallHint { get; set; } + + /// + /// Whether or not to provide cluster information to this exec plugin as a part of + /// the KUBERNETES_EXEC_INFO environment variable. Optional. + /// + [YamlMember(Alias = "provideClusterInfo")] + public bool ProvideClusterInfo { get; set; } + } +} diff --git a/src/KubernetesClient.Aot/KubeConfigModels/K8SConfiguration.cs b/src/KubernetesClient.Aot/KubeConfigModels/K8SConfiguration.cs new file mode 100644 index 000000000..a0c3a4fef --- /dev/null +++ b/src/KubernetesClient.Aot/KubeConfigModels/K8SConfiguration.cs @@ -0,0 +1,65 @@ +using YamlDotNet.Serialization; + +namespace k8s.KubeConfigModels +{ + /// + /// kubeconfig configuration model. Holds the information needed to build connect to remote + /// Kubernetes clusters as a given user. + /// + /// + /// Should be kept in sync with https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go + /// Should update MergeKubeConfig in KubernetesClientConfiguration.ConfigFile.cs if updated. + /// + [YamlSerializable] + public class K8SConfiguration + { + // /// + // /// Gets or sets general information to be use for CLI interactions + // /// + // [YamlMember(Alias = "preferences")] + // public IDictionary Preferences { get; set; } + + [YamlMember(Alias = "apiVersion")] + public string ApiVersion { get; set; } + + [YamlMember(Alias = "kind")] + public string Kind { get; set; } + + /// + /// Gets or sets the name of the context that you would like to use by default. + /// + [YamlMember(Alias = "current-context", ApplyNamingConventions = false)] + public string CurrentContext { get; set; } + + /// + /// Gets or sets a map of referencable names to context configs. + /// + [YamlMember(Alias = "contexts")] + public List Contexts { get; set; } = new List(); + + /// + /// Gets or sets a map of referencable names to cluster configs. + /// + [YamlMember(Alias = "clusters")] + public List Clusters { get; set; } = new List(); + + /// + /// Gets or sets a map of referencable names to user configs + /// + [YamlMember(Alias = "users")] + public List Users { get; set; } = new List(); + + // /// + // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + // /// + // [YamlMember(Alias = "extensions")] + // public List Extensions { get; set; } + + /// + /// Gets or sets the name of the Kubernetes configuration file. This property is set only when the configuration + /// was loaded from disk, and can be used to resolve relative paths. + /// + [YamlIgnore] + public string FileName { get; set; } + } +} diff --git a/src/KubernetesClient/KubeConfigModels/StaticContext.cs b/src/KubernetesClient.Aot/KubeConfigModels/StaticContext.cs similarity index 100% rename from src/KubernetesClient/KubeConfigModels/StaticContext.cs rename to src/KubernetesClient.Aot/KubeConfigModels/StaticContext.cs diff --git a/src/KubernetesClient.Aot/KubeConfigModels/User.cs b/src/KubernetesClient.Aot/KubeConfigModels/User.cs new file mode 100644 index 000000000..557f02256 --- /dev/null +++ b/src/KubernetesClient.Aot/KubeConfigModels/User.cs @@ -0,0 +1,23 @@ +using YamlDotNet.Serialization; + +namespace k8s.KubeConfigModels +{ + /// + /// Relates nicknames to auth information. + /// + [YamlSerializable] + public class User + { + /// + /// Gets or sets the auth information. + /// + [YamlMember(Alias = "user")] + public UserCredentials UserCredentials { get; set; } + + /// + /// Gets or sets the nickname for this auth information. + /// + [YamlMember(Alias = "name")] + public string Name { get; set; } + } +} diff --git a/src/KubernetesClient.Aot/KubeConfigModels/UserCredentials.cs b/src/KubernetesClient.Aot/KubeConfigModels/UserCredentials.cs new file mode 100644 index 000000000..8ef46fc95 --- /dev/null +++ b/src/KubernetesClient.Aot/KubeConfigModels/UserCredentials.cs @@ -0,0 +1,89 @@ +using YamlDotNet.Serialization; + +namespace k8s.KubeConfigModels +{ + /// + /// Contains information that describes identity information. This is use to tell the kubernetes cluster who you are. + /// + [YamlSerializable] + public class UserCredentials + { + /// + /// Gets or sets PEM-encoded data from a client cert file for TLS. Overrides . + /// + [YamlMember(Alias = "client-certificate-data", ApplyNamingConventions = false)] + public string ClientCertificateData { get; set; } + + /// + /// Gets or sets the path to a client cert file for TLS. + /// + [YamlMember(Alias = "client-certificate", ApplyNamingConventions = false)] + public string ClientCertificate { get; set; } + + /// + /// Gets or sets PEM-encoded data from a client key file for TLS. Overrides . + /// + [YamlMember(Alias = "client-key-data", ApplyNamingConventions = false)] + public string ClientKeyData { get; set; } + + /// + /// Gets or sets the path to a client key file for TLS. + /// + [YamlMember(Alias = "client-key", ApplyNamingConventions = false)] + public string ClientKey { get; set; } + + /// + /// Gets or sets the bearer token for authentication to the kubernetes cluster. + /// + [YamlMember(Alias = "token")] + public string Token { get; set; } + + /// + /// Gets or sets the username to impersonate. The name matches the flag. + /// + [YamlMember(Alias = "as")] + public string Impersonate { get; set; } + + /// + /// Gets or sets the groups to impersonate. + /// + [YamlMember(Alias = "as-groups", ApplyNamingConventions = false)] + public IEnumerable ImpersonateGroups { get; set; } = new string[0]; + + /// + /// Gets or sets additional information for impersonated user. + /// + [YamlMember(Alias = "as-user-extra", ApplyNamingConventions = false)] + public Dictionary ImpersonateUserExtra { get; set; } = new Dictionary(); + + /// + /// Gets or sets the username for basic authentication to the kubernetes cluster. + /// + [YamlMember(Alias = "username")] + public string UserName { get; set; } + + /// + /// Gets or sets the password for basic authentication to the kubernetes cluster. + /// + [YamlMember(Alias = "password")] + public string Password { get; set; } + + /// + /// Gets or sets custom authentication plugin for the kubernetes cluster. + /// + [YamlMember(Alias = "auth-provider", ApplyNamingConventions = false)] + public AuthProvider AuthProvider { get; set; } + + // /// + // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + // /// + // [YamlMember(Alias = "extensions")] + // public IEnumerable Extensions { get; set; } + + /// + /// Gets or sets external command and its arguments to receive user credentials + /// + [YamlMember(Alias = "exec")] + public ExternalExecution ExternalExecution { get; set; } + } +} diff --git a/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj b/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj new file mode 100644 index 000000000..cb04b85b2 --- /dev/null +++ b/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj @@ -0,0 +1,114 @@ + + + + net8.0 + k8s + true + true + true + AOT_ENABLED + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs b/src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs new file mode 100644 index 000000000..597eea7c5 --- /dev/null +++ b/src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs @@ -0,0 +1,760 @@ +using k8s.Authentication; +using k8s.Exceptions; +using k8s.KubeConfigModels; +using System.Diagnostics; +using System.Net; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; + +namespace k8s +{ + public partial class KubernetesClientConfiguration + { + /// + /// kubeconfig Default Location + /// + public static readonly string KubeConfigDefaultLocation = + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE") ?? @"\", @".kube\config") + : Path.Combine(Environment.GetEnvironmentVariable("HOME") ?? "/", ".kube/config"); + + /// + /// Gets CurrentContext + /// + public string CurrentContext { get; private set; } + + // For testing + internal static string KubeConfigEnvironmentVariable { get; set; } = "KUBECONFIG"; + + /// + /// Exec process timeout + /// + public static TimeSpan ExecTimeout { get; set; } = TimeSpan.FromMinutes(2); + + /// + /// Exec process Standard Errors + /// + public static event EventHandler ExecStdError; + + /// + /// Initializes a new instance of the from default locations + /// If the KUBECONFIG environment variable is set, then that will be used. + /// Next, it looks for a config file at . + /// Then, it checks whether it is executing inside a cluster and will use . + /// Finally, if nothing else exists, it creates a default config with localhost:8080 as host. + /// + /// + /// If multiple kubeconfig files are specified in the KUBECONFIG environment variable, + /// merges the files, where first occurrence wins. See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files. + /// + /// Instance of the class + public static KubernetesClientConfiguration BuildDefaultConfig() + { + var kubeconfig = Environment.GetEnvironmentVariable(KubeConfigEnvironmentVariable); + if (kubeconfig != null) + { + var configList = kubeconfig.Split(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ';' : ':') + .Select((s) => new FileInfo(s.Trim('"'))); + var k8sConfig = LoadKubeConfig(configList.ToArray()); + return BuildConfigFromConfigObject(k8sConfig); + } + + if (File.Exists(KubeConfigDefaultLocation)) + { + return BuildConfigFromConfigFile(KubeConfigDefaultLocation); + } + + if (IsInCluster()) + { + return InClusterConfig(); + } + + var config = new KubernetesClientConfiguration + { + Host = "http://localhost:8080", + }; + + return config; + } + + /// + /// Initializes a new instance of the from config file + /// + /// Explicit file path to kubeconfig. Set to null to use the default file path + /// override the context in config file, set null if do not want to override + /// kube api server endpoint + /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig + /// file is located. When , the paths will be considered to be relative to the current working directory. + /// Instance of the class + public static KubernetesClientConfiguration BuildConfigFromConfigFile( + string kubeconfigPath = null, + string currentContext = null, string masterUrl = null, bool useRelativePaths = true) + { + return BuildConfigFromConfigFile(new FileInfo(kubeconfigPath ?? KubeConfigDefaultLocation), currentContext, + masterUrl, useRelativePaths); + } + + /// + /// Initializes a new instance of the from config file + /// + /// Fileinfo of the kubeconfig, cannot be null + /// override the context in config file, set null if do not want to override + /// override the kube api server endpoint, set null if do not want to override + /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig + /// file is located. When , the paths will be considered to be relative to the current working directory. + /// Instance of the class + public static KubernetesClientConfiguration BuildConfigFromConfigFile( + FileInfo kubeconfig, + string currentContext = null, string masterUrl = null, bool useRelativePaths = true) + { + return BuildConfigFromConfigFileAsync(kubeconfig, currentContext, masterUrl, useRelativePaths).GetAwaiter() + .GetResult(); + } + + /// + /// Initializes a new instance of the from config file + /// + /// Fileinfo of the kubeconfig, cannot be null + /// override the context in config file, set null if do not want to override + /// override the kube api server endpoint, set null if do not want to override + /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig + /// file is located. When , the paths will be considered to be relative to the current working directory. + /// Instance of the class + public static async Task BuildConfigFromConfigFileAsync( + FileInfo kubeconfig, + string currentContext = null, string masterUrl = null, bool useRelativePaths = true) + { + if (kubeconfig == null) + { + throw new NullReferenceException(nameof(kubeconfig)); + } + + var k8SConfig = await LoadKubeConfigAsync(kubeconfig, useRelativePaths).ConfigureAwait(false); + var k8SConfiguration = GetKubernetesClientConfiguration(currentContext, masterUrl, k8SConfig); + + return k8SConfiguration; + } + + /// + /// Initializes a new instance of the from config file + /// + /// Stream of the kubeconfig, cannot be null + /// Override the current context in config, set null if do not want to override + /// Override the Kubernetes API server endpoint, set null if do not want to override + /// Instance of the class + public static KubernetesClientConfiguration BuildConfigFromConfigFile( + Stream kubeconfig, + string currentContext = null, string masterUrl = null) + { + return BuildConfigFromConfigFileAsync(kubeconfig, currentContext, masterUrl).GetAwaiter().GetResult(); + } + + /// + /// Initializes a new instance of the from config file + /// + /// Stream of the kubeconfig, cannot be null + /// Override the current context in config, set null if do not want to override + /// Override the Kubernetes API server endpoint, set null if do not want to override + /// Instance of the class + public static async Task BuildConfigFromConfigFileAsync( + Stream kubeconfig, + string currentContext = null, string masterUrl = null) + { + if (kubeconfig == null) + { + throw new NullReferenceException(nameof(kubeconfig)); + } + + if (!kubeconfig.CanSeek) + { + throw new Exception("Stream don't support seeking!"); + } + + kubeconfig.Position = 0; + + var k8SConfig = await KubernetesYaml.LoadFromStreamAsync(kubeconfig).ConfigureAwait(false); + var k8SConfiguration = GetKubernetesClientConfiguration(currentContext, masterUrl, k8SConfig); + + return k8SConfiguration; + } + + /// + /// Initializes a new instance of from pre-loaded config object. + /// + /// A , for example loaded from + /// Override the current context in config, set null if do not want to override + /// Override the Kubernetes API server endpoint, set null if do not want to override + /// Instance of the class + public static KubernetesClientConfiguration BuildConfigFromConfigObject( + K8SConfiguration k8SConfig, + string currentContext = null, string masterUrl = null) + => GetKubernetesClientConfiguration(currentContext, masterUrl, k8SConfig); + + private static KubernetesClientConfiguration GetKubernetesClientConfiguration( + string currentContext, + string masterUrl, K8SConfiguration k8SConfig) + { + if (k8SConfig == null) + { + throw new ArgumentNullException(nameof(k8SConfig)); + } + + var k8SConfiguration = new KubernetesClientConfiguration(); + + currentContext = currentContext ?? k8SConfig.CurrentContext; + // only init context if context is set + if (currentContext != null) + { + k8SConfiguration.InitializeContext(k8SConfig, currentContext); + } + + if (!string.IsNullOrWhiteSpace(masterUrl)) + { + k8SConfiguration.Host = masterUrl; + } + + if (string.IsNullOrWhiteSpace(k8SConfiguration.Host)) + { + throw new KubeConfigException("Cannot infer server host url either from context or masterUrl"); + } + + return k8SConfiguration; + } + + /// + /// Validates and Initializes Client Configuration + /// + /// Kubernetes Configuration + /// Current Context + private void InitializeContext(K8SConfiguration k8SConfig, string currentContext) + { + // current context + var activeContext = + k8SConfig.Contexts.FirstOrDefault( + c => c.Name.Equals(currentContext, StringComparison.OrdinalIgnoreCase)); + if (activeContext == null) + { + throw new KubeConfigException($"CurrentContext: {currentContext} not found in contexts in kubeconfig"); + } + + if (string.IsNullOrEmpty(activeContext.ContextDetails?.Cluster)) + { + // This serves as validation for any of the properties of ContextDetails being set. + // Other locations in code assume that ContextDetails is non-null. + throw new KubeConfigException($"Cluster not set for context `{currentContext}` in kubeconfig"); + } + + CurrentContext = activeContext.Name; + + // cluster + SetClusterDetails(k8SConfig, activeContext); + + // user + SetUserDetails(k8SConfig, activeContext); + + // namespace + Namespace = activeContext.ContextDetails?.Namespace; + } + + private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext) + { + var clusterDetails = + k8SConfig.Clusters.FirstOrDefault(c => c.Name.Equals( + activeContext.ContextDetails.Cluster, + StringComparison.OrdinalIgnoreCase)); + + if (clusterDetails?.ClusterEndpoint == null) + { + throw new KubeConfigException($"Cluster not found for context `{activeContext}` in kubeconfig"); + } + + if (string.IsNullOrWhiteSpace(clusterDetails.ClusterEndpoint.Server)) + { + throw new KubeConfigException($"Server not found for current-context `{activeContext}` in kubeconfig"); + } + + Host = clusterDetails.ClusterEndpoint.Server; + SkipTlsVerify = clusterDetails.ClusterEndpoint.SkipTlsVerify; + TlsServerName = clusterDetails.ClusterEndpoint.TlsServerName; + + if (!Uri.TryCreate(Host, UriKind.Absolute, out var uri)) + { + throw new KubeConfigException($"Bad server host URL `{Host}` (cannot be parsed)"); + } + + if (IPAddress.TryParse(uri.Host, out var ipAddress)) + { + if (IPAddress.Equals(IPAddress.Any, ipAddress)) + { + var builder = new UriBuilder(Host) + { + Host = $"{IPAddress.Loopback}", + }; + Host = builder.ToString(); + } + else if (IPAddress.Equals(IPAddress.IPv6Any, ipAddress)) + { + var builder = new UriBuilder(Host) + { + Host = $"{IPAddress.IPv6Loopback}", + }; + Host = builder.ToString(); + } + } + + if (uri.Scheme == "https") + { + if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthorityData)) + { + // This null password is to change the constructor to fix this KB: + // https://support.microsoft.com/en-us/topic/kb5025823-change-in-how-net-applications-import-x-509-certificates-bf81c936-af2b-446e-9f7a-016f4713b46b + string nullPassword = null; + var data = clusterDetails.ClusterEndpoint.CertificateAuthorityData; + SslCaCerts = new X509Certificate2Collection(new X509Certificate2(Convert.FromBase64String(data), nullPassword)); + } + else if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthority)) + { + SslCaCerts = new X509Certificate2Collection(new X509Certificate2(GetFullPath( + k8SConfig, + clusterDetails.ClusterEndpoint.CertificateAuthority))); + } + } + } + + private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext) + { + if (string.IsNullOrWhiteSpace(activeContext.ContextDetails.User)) + { + return; + } + + var userDetails = k8SConfig.Users.FirstOrDefault(c => c.Name.Equals( + activeContext.ContextDetails.User, + StringComparison.OrdinalIgnoreCase)); + + if (userDetails == null) + { + throw new KubeConfigException($"User not found for context {activeContext.Name} in kubeconfig"); + } + + if (userDetails.UserCredentials == null) + { + throw new KubeConfigException($"User credentials not found for user: {userDetails.Name} in kubeconfig"); + } + + var userCredentialsFound = false; + + // Basic and bearer tokens are mutually exclusive + if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.Token)) + { + AccessToken = userDetails.UserCredentials.Token; + userCredentialsFound = true; + } + else if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.UserName) && + !string.IsNullOrWhiteSpace(userDetails.UserCredentials.Password)) + { + Username = userDetails.UserCredentials.UserName; + Password = userDetails.UserCredentials.Password; + userCredentialsFound = true; + } + + // Token and cert based auth can co-exist + if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientCertificateData) && + !string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientKeyData)) + { + ClientCertificateData = userDetails.UserCredentials.ClientCertificateData; + ClientCertificateKeyData = userDetails.UserCredentials.ClientKeyData; + userCredentialsFound = true; + } + + if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientCertificate) && + !string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientKey)) + { + ClientCertificateFilePath = GetFullPath(k8SConfig, userDetails.UserCredentials.ClientCertificate); + ClientKeyFilePath = GetFullPath(k8SConfig, userDetails.UserCredentials.ClientKey); + userCredentialsFound = true; + } + + if (userDetails.UserCredentials.AuthProvider != null) + { + if (userDetails.UserCredentials.AuthProvider.Config != null + && (userDetails.UserCredentials.AuthProvider.Config.ContainsKey("access-token") + || userDetails.UserCredentials.AuthProvider.Config.ContainsKey("id-token"))) + { + switch (userDetails.UserCredentials.AuthProvider.Name) + { + case "azure": + throw new Exception("Please use the https://github.com/Azure/kubelogin credential plugin instead. See https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins for further details`"); + + case "gcp": + throw new Exception("Please use the \"gke-gcloud-auth-plugin\" credential plugin instead. See https://cloud.google.com/blog/products/containers-kubernetes/kubectl-auth-changes-in-gke for further details"); + } + } + } + + if (userDetails.UserCredentials.ExternalExecution != null) + { + if (string.IsNullOrWhiteSpace(userDetails.UserCredentials.ExternalExecution.Command)) + { + throw new KubeConfigException( + "External command execution to receive user credentials must include a command to execute"); + } + + if (string.IsNullOrWhiteSpace(userDetails.UserCredentials.ExternalExecution.ApiVersion)) + { + throw new KubeConfigException("External command execution missing ApiVersion key"); + } + + var response = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution); + AccessToken = response.Status.Token; + // When reading ClientCertificateData from a config file it will be base64 encoded, and code later in the system (see CertUtils.GeneratePfx) + // expects ClientCertificateData and ClientCertificateKeyData to be base64 encoded because of this. However the string returned by external + // auth providers is the raw certificate and key PEM text, so we need to take that and base64 encoded it here so it can be decoded later. + ClientCertificateData = response.Status.ClientCertificateData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(response.Status.ClientCertificateData)); + ClientCertificateKeyData = response.Status.ClientKeyData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(response.Status.ClientKeyData)); + + userCredentialsFound = true; + + // TODO: support client certificates here too. + if (AccessToken != null) + { + TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution); + } + } + + if (!userCredentialsFound) + { + throw new KubeConfigException( + $"User: {userDetails.Name} does not have appropriate auth credentials in kubeconfig"); + } + } + + public static Process CreateRunnableExternalProcess(ExternalExecution config, EventHandler captureStdError = null) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + var process = new Process(); + + process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", $"{{ \"apiVersion\":\"{config.ApiVersion}\",\"kind\":\"ExecCredentials\",\"spec\":{{ \"interactive\":{Environment.UserInteractive.ToString().ToLower()} }} }}"); + if (config.EnvironmentVariables != null) + { + foreach (var configEnvironmentVariable in config.EnvironmentVariables) + { + if (configEnvironmentVariable.ContainsKey("name") && configEnvironmentVariable.ContainsKey("value")) + { + var name = configEnvironmentVariable["name"]; + process.StartInfo.EnvironmentVariables[name] = configEnvironmentVariable["value"]; + } + else + { + var badVariable = string.Join(",", configEnvironmentVariable.Select(x => $"{x.Key}={x.Value}")); + throw new KubeConfigException($"Invalid environment variable defined: {badVariable}"); + } + } + } + + process.StartInfo.FileName = config.Command; + if (config.Arguments != null) + { + process.StartInfo.Arguments = string.Join(" ", config.Arguments); + } + + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = captureStdError != null; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + + return process; + } + + /// + /// Implementation of the proposal for out-of-tree client + /// authentication providers as described here -- + /// https://github.com/kubernetes/community/blob/master/contributors/design-proposals/auth/kubectl-exec-plugins.md + /// Took inspiration from python exec_provider.py -- + /// https://github.com/kubernetes-client/python-base/blob/master/config/exec_provider.py + /// + /// The external command execution configuration + /// + /// The token, client certificate data, and the client key data received from the external command execution + /// + public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + var captureStdError = ExecStdError; + var process = CreateRunnableExternalProcess(config, captureStdError); + + try + { + process.Start(); + if (captureStdError != null) + { + process.ErrorDataReceived += captureStdError.Invoke; + process.BeginErrorReadLine(); + } + } + catch (Exception ex) + { + throw new KubeConfigException($"external exec failed due to: {ex.Message}"); + } + + try + { + if (!process.WaitForExit((int)(ExecTimeout.TotalMilliseconds))) + { + throw new KubeConfigException("external exec failed due to timeout"); + } + + var responseObject = KubernetesJson.Deserialize(process.StandardOutput.ReadToEnd()); + if (responseObject == null || responseObject.ApiVersion != config.ApiVersion) + { + throw new KubeConfigException( + $"external exec failed because api version {responseObject.ApiVersion} does not match {config.ApiVersion}"); + } + + if (responseObject.Status.IsValid()) + { + return responseObject; + } + else + { + throw new KubeConfigException($"external exec failed missing token or clientCertificateData field in plugin output"); + } + } + catch (JsonException ex) + { + throw new KubeConfigException($"external exec failed due to failed deserialization process: {ex}"); + } + catch (Exception ex) + { + throw new KubeConfigException($"external exec failed due to uncaught exception: {ex}"); + } + } + + /// + /// Loads entire Kube Config from default or explicit file path + /// + /// Explicit file path to kubeconfig. Set to null to use the default file path + /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig + /// file is located. When , the paths will be considered to be relative to the current working directory. + /// Instance of the class + public static async Task LoadKubeConfigAsync( + string kubeconfigPath = null, + bool useRelativePaths = true) + { + var fileInfo = new FileInfo(kubeconfigPath ?? KubeConfigDefaultLocation); + + return await LoadKubeConfigAsync(fileInfo, useRelativePaths).ConfigureAwait(false); + } + + /// + /// Loads entire Kube Config from default or explicit file path + /// + /// Explicit file path to kubeconfig. Set to null to use the default file path + /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig + /// file is located. When , the paths will be considered to be relative to the current working directory. + /// Instance of the class + public static K8SConfiguration LoadKubeConfig(string kubeconfigPath = null, bool useRelativePaths = true) + { + return LoadKubeConfigAsync(kubeconfigPath, useRelativePaths).GetAwaiter().GetResult(); + } + + /// + /// Loads Kube Config + /// + /// Kube config file contents + /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig + /// file is located. When , the paths will be considered to be relative to the current working directory. + /// Instance of the class + public static async Task LoadKubeConfigAsync( + FileInfo kubeconfig, + bool useRelativePaths = true) + { + if (kubeconfig == null) + { + throw new ArgumentNullException(nameof(kubeconfig)); + } + + + if (!kubeconfig.Exists) + { + throw new KubeConfigException($"kubeconfig file not found at {kubeconfig.FullName}"); + } + + using (var stream = kubeconfig.OpenRead()) + { + var config = await KubernetesYaml.LoadFromStreamAsync(stream).ConfigureAwait(false); + + if (useRelativePaths) + { + config.FileName = kubeconfig.FullName; + } + + return config; + } + } + + /// + /// Loads Kube Config + /// + /// Kube config file contents + /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig + /// file is located. When , the paths will be considered to be relative to the current working directory. + /// Instance of the class + public static K8SConfiguration LoadKubeConfig(FileInfo kubeconfig, bool useRelativePaths = true) + { + return LoadKubeConfigAsync(kubeconfig, useRelativePaths).GetAwaiter().GetResult(); + } + + /// + /// Loads Kube Config + /// + /// Kube config file contents stream + /// Instance of the class + public static async Task LoadKubeConfigAsync(Stream kubeconfigStream) + { + return await KubernetesYaml.LoadFromStreamAsync(kubeconfigStream).ConfigureAwait(false); + } + + /// + /// Loads Kube Config + /// + /// Kube config file contents stream + /// Instance of the class + public static K8SConfiguration LoadKubeConfig(Stream kubeconfigStream) + { + return LoadKubeConfigAsync(kubeconfigStream).GetAwaiter().GetResult(); + } + + /// + /// Loads Kube Config + /// + /// List of kube config file contents + /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig + /// file is located. When , the paths will be considered to be relative to the current working directory. + /// Instance of the class + /// + /// The kube config files will be merges into a single , where first occurrence wins. + /// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files. + /// + internal static K8SConfiguration LoadKubeConfig(FileInfo[] kubeConfigs, bool useRelativePaths = true) + { + return LoadKubeConfigAsync(kubeConfigs, useRelativePaths).GetAwaiter().GetResult(); + } + + /// + /// Loads Kube Config + /// + /// List of kube config file contents + /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig + /// file is located. When , the paths will be considered to be relative to the current working directory. + /// Instance of the class + /// + /// The kube config files will be merges into a single , where first occurrence wins. + /// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files. + /// + internal static async Task LoadKubeConfigAsync( + FileInfo[] kubeConfigs, + bool useRelativePaths = true) + { + var basek8SConfig = await LoadKubeConfigAsync(kubeConfigs[0], useRelativePaths).ConfigureAwait(false); + + for (var i = 1; i < kubeConfigs.Length; i++) + { + var mergek8SConfig = await LoadKubeConfigAsync(kubeConfigs[i], useRelativePaths).ConfigureAwait(false); + MergeKubeConfig(basek8SConfig, mergek8SConfig); + } + + return basek8SConfig; + } + + /// + /// Tries to get the full path to a file referenced from the Kubernetes configuration. + /// + /// + /// The Kubernetes configuration. + /// + /// + /// The path to resolve. + /// + /// + /// When possible a fully qualified path to the file. + /// + /// + /// For example, if the configuration file is at "C:\Users\me\kube.config" and path is "ca.crt", + /// this will return "C:\Users\me\ca.crt". Similarly, if path is "D:\ca.cart", this will return + /// "D:\ca.crt". + /// + private static string GetFullPath(K8SConfiguration configuration, string path) + { + // If we don't have a file name, + if (string.IsNullOrWhiteSpace(configuration.FileName) || Path.IsPathRooted(path)) + { + return path; + } + else + { + return Path.Combine(Path.GetDirectoryName(configuration.FileName), path); + } + } + + /// + /// Merges kube config files together, preferring configuration present in the base config over the merge config. + /// + /// The to merge into + /// The to merge from + private static void MergeKubeConfig(K8SConfiguration basek8SConfig, K8SConfiguration mergek8SConfig) + { + // For scalar values, prefer local values + basek8SConfig.CurrentContext = basek8SConfig.CurrentContext ?? mergek8SConfig.CurrentContext; + basek8SConfig.FileName = basek8SConfig.FileName ?? mergek8SConfig.FileName; + + // Kinds must match in kube config, otherwise throw. + if (basek8SConfig.Kind != mergek8SConfig.Kind) + { + throw new KubeConfigException( + $"kubeconfig \"kind\" are different between {basek8SConfig.FileName} and {mergek8SConfig.FileName}"); + } + + // Note, Clusters, Contexts, and Extensions are map-like in config despite being represented as a list here: + // https://github.com/kubernetes/client-go/blob/ede92e0fe62deed512d9ceb8bf4186db9f3776ff/tools/clientcmd/api/types.go#L238 + // basek8SConfig.Extensions = MergeLists(basek8SConfig.Extensions, mergek8SConfig.Extensions, (s) => s.Name).ToList(); + basek8SConfig.Clusters = MergeLists(basek8SConfig.Clusters, mergek8SConfig.Clusters, (s) => s.Name).ToList(); + basek8SConfig.Users = MergeLists(basek8SConfig.Users, mergek8SConfig.Users, (s) => s.Name).ToList(); + basek8SConfig.Contexts = MergeLists(basek8SConfig.Contexts, mergek8SConfig.Contexts, (s) => s.Name).ToList(); + } + + private static IEnumerable MergeLists(IEnumerable baseList, IEnumerable mergeList, + Func getNameFunc) + { + if (mergeList != null && mergeList.Any()) + { + var mapping = new Dictionary(); + foreach (var item in baseList) + { + mapping[getNameFunc(item)] = item; + } + + foreach (var item in mergeList) + { + var name = getNameFunc(item); + if (!mapping.ContainsKey(name)) + { + mapping[name] = item; + } + } + + return mapping.Values; + } + + return baseList; + } + } +} diff --git a/src/KubernetesClient.Aot/KubernetesJson.cs b/src/KubernetesClient.Aot/KubernetesJson.cs new file mode 100644 index 000000000..d7c0b3062 --- /dev/null +++ b/src/KubernetesClient.Aot/KubernetesJson.cs @@ -0,0 +1,118 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using System.Xml; + +namespace k8s +{ + internal static class KubernetesJson + { + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions(); + + private sealed class Iso8601TimeSpanConverter : JsonConverter + { + 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); + } + } + + private sealed class KubernetesDateTimeOffsetConverter : JsonConverter + { + 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)); + } + } + + private sealed class KubernetesDateTimeConverter : JsonConverter + { + private static readonly JsonConverter 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); + } + } + + static KubernetesJson() + { + 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()); + +#if NET8_0_OR_GREATER + JsonSerializerOptions.TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? new System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver() : SourceGenerationContext.Default; +#endif + } + + /// + /// Configures for the . + /// To override existing converters, add them to the top of the list + /// e.g. as follows: options.Converters.Insert(index: 0, new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); + /// + /// An to configure the . + public static void AddJsonOptions(Action configure) + { + if (configure is null) + { + throw new ArgumentNullException(nameof(configure)); + } + + configure(JsonSerializerOptions); + } + + public static TValue Deserialize(string json, JsonSerializerOptions jsonSerializerOptions = null) + { + return JsonSerializer.Deserialize(json, jsonSerializerOptions ?? JsonSerializerOptions); + } + + public static TValue Deserialize(Stream json, JsonSerializerOptions jsonSerializerOptions = null) + { + return JsonSerializer.Deserialize(json, jsonSerializerOptions ?? JsonSerializerOptions); + } + + public static string Serialize(object value, JsonSerializerOptions jsonSerializerOptions = null) + { + return JsonSerializer.Serialize(value, jsonSerializerOptions ?? JsonSerializerOptions); + } + } +} diff --git a/src/KubernetesClient.Aot/KubernetesYaml.cs b/src/KubernetesClient.Aot/KubernetesYaml.cs new file mode 100644 index 000000000..2ae6405c6 --- /dev/null +++ b/src/KubernetesClient.Aot/KubernetesYaml.cs @@ -0,0 +1,255 @@ +using System.Reflection; +using System.Text; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace k8s +{ + /// + /// This is a utility class that helps you load objects from YAML files. + /// + internal static class KubernetesYaml + { + private static StaticDeserializerBuilder CommonDeserializerBuilder => + new StaticDeserializerBuilder(new k8s.KubeConfigModels.StaticContext()) + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeConverter(new IntOrStringYamlConverter()) + .WithTypeConverter(new ByteArrayStringYamlConverter()) + .WithTypeConverter(new ResourceQuantityYamlConverter()) + .WithAttemptingUnquotedStringTypeDeserialization() + ; + + private static readonly IDeserializer Deserializer = + CommonDeserializerBuilder + .IgnoreUnmatchedProperties() + .Build(); + private static IDeserializer GetDeserializer(bool strict) => Deserializer; + + private static readonly IValueSerializer Serializer = + new StaticSerializerBuilder(new k8s.KubeConfigModels.StaticContext()) + .DisableAliases() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeConverter(new IntOrStringYamlConverter()) + .WithTypeConverter(new ByteArrayStringYamlConverter()) + .WithTypeConverter(new ResourceQuantityYamlConverter()) + .WithEventEmitter(e => new StringQuotingEmitter(e)) + .WithEventEmitter(e => new FloatEmitter(e)) + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) + .BuildValueSerializer(); + + private static readonly IDictionary ModelTypeMap = typeof(KubernetesEntityAttribute).Assembly + .GetTypes() + .Where(t => t.GetCustomAttributes(typeof(KubernetesEntityAttribute), true).Any()) + .ToDictionary( + t => + { + var attr = (KubernetesEntityAttribute)t.GetCustomAttribute( + typeof(KubernetesEntityAttribute), true); + var groupPrefix = string.IsNullOrEmpty(attr.Group) ? "" : $"{attr.Group}/"; + return $"{groupPrefix}{attr.ApiVersion}/{attr.Kind}"; + }, + t => t); + + private class ByteArrayStringYamlConverter : IYamlTypeConverter + { + public bool Accepts(Type type) + { + return type == typeof(byte[]); + } + + public object ReadYaml(IParser parser, Type type) + { + if (parser?.Current is Scalar scalar) + { + try + { + if (string.IsNullOrEmpty(scalar.Value)) + { + return null; + } + + return Encoding.UTF8.GetBytes(scalar.Value); + } + finally + { + parser.MoveNext(); + } + } + + throw new InvalidOperationException(parser.Current?.ToString()); + } + + public void WriteYaml(IEmitter emitter, object value, Type type) + { + var obj = (byte[])value; + emitter?.Emit(new Scalar(Encoding.UTF8.GetString(obj))); + } + } + + /// + /// Load a collection of objects from a stream asynchronously + /// + /// caller is responsible for closing the stream + /// + /// + /// The stream to load the objects from. + /// + /// + /// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will + /// be used. + /// + /// true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise + /// collection of objects + public static async Task> LoadAllFromStreamAsync(Stream stream, IDictionary typeMap = null, bool strict = false) + { + var reader = new StreamReader(stream); + var content = await reader.ReadToEndAsync().ConfigureAwait(false); + return LoadAllFromString(content, typeMap); + } + + + /// + /// Load a collection of objects from a file asynchronously + /// + /// The name of the file to load from. + /// + /// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will + /// be used. + /// + /// true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise + /// collection of objects + public static async Task> LoadAllFromFileAsync(string fileName, IDictionary typeMap = null, bool strict = false) + { + using (var fileStream = File.OpenRead(fileName)) + { + return await LoadAllFromStreamAsync(fileStream, typeMap).ConfigureAwait(false); + } + } + + /// + /// Load a collection of objects from a string + /// + /// + /// The string to load the objects from. + /// + /// + /// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will + /// be used. + /// + /// true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise + /// collection of objects + public static List LoadAllFromString(string content, IDictionary typeMap = null, bool strict = false) + { + var mergedTypeMap = new Dictionary(ModelTypeMap); + // merge in KVPs from typeMap, overriding any in ModelTypeMap + typeMap?.ToList().ForEach(x => mergedTypeMap[x.Key] = x.Value); + + var types = new List(); + var parser = new MergingParser(new Parser(new StringReader(content))); + parser.Consume(); + while (parser.Accept(out _)) + { + var dict = GetDeserializer(strict).Deserialize>(parser); + types.Add(mergedTypeMap[dict["apiVersion"] + "/" + dict["kind"]]); + } + + parser = new MergingParser(new Parser(new StringReader(content))); + parser.Consume(); + var ix = 0; + var results = new List(); + while (parser.Accept(out _)) + { + var objType = types[ix++]; + var obj = GetDeserializer(strict).Deserialize(parser, objType); + results.Add(obj); + } + + return results; + } + + public static async Task LoadFromStreamAsync(Stream stream, bool strict = false) + { + var reader = new StreamReader(stream); + var content = await reader.ReadToEndAsync().ConfigureAwait(false); + return Deserialize(content, strict); + } + + public static async Task LoadFromFileAsync(string file, bool strict = false) + { + using (var fs = File.OpenRead(file)) + { + return await LoadFromStreamAsync(fs, strict).ConfigureAwait(false); + } + } + + [Obsolete("use Deserialize")] + public static T LoadFromString(string content, bool strict = false) + { + return Deserialize(content, strict); + } + + [Obsolete("use Serialize")] + public static string SaveToString(T value) + { + return Serialize(value); + } + + public static TValue Deserialize(string yaml, bool strict = false) + { + using var reader = new StringReader(yaml); + return GetDeserializer(strict).Deserialize(new MergingParser(new Parser(reader))); + } + + public static TValue Deserialize(Stream yaml, bool strict = false) + { + using var reader = new StreamReader(yaml); + return GetDeserializer(strict).Deserialize(new MergingParser(new Parser(reader))); + } + + public static string SerializeAll(IEnumerable values) + { + if (values == null) + { + return ""; + } + + var stringBuilder = new StringBuilder(); + var writer = new StringWriter(stringBuilder); + var emitter = new Emitter(writer); + + emitter.Emit(new StreamStart()); + + foreach (var value in values) + { + if (value != null) + { + emitter.Emit(new DocumentStart()); + Serializer.SerializeValue(emitter, value, value.GetType()); + emitter.Emit(new DocumentEnd(true)); + } + } + + return stringBuilder.ToString(); + } + + public static string Serialize(object value) + { + if (value == null) + { + return ""; + } + + var stringBuilder = new StringBuilder(); + var writer = new StringWriter(stringBuilder); + var emitter = new Emitter(writer); + + emitter.Emit(new StreamStart()); + emitter.Emit(new DocumentStart()); + Serializer.SerializeValue(emitter, value, value.GetType()); + + return stringBuilder.ToString(); + } + } +} diff --git a/src/KubernetesClient/KubeConfigModels/AuthProvider.cs b/src/KubernetesClient/KubeConfigModels/AuthProvider.cs index 5bec9095e..4ec5d94e0 100644 --- a/src/KubernetesClient/KubeConfigModels/AuthProvider.cs +++ b/src/KubernetesClient/KubeConfigModels/AuthProvider.cs @@ -5,7 +5,6 @@ namespace k8s.KubeConfigModels /// /// Contains information that describes identity information. This is use to tell the kubernetes cluster who you are. /// - [YamlSerializable] public class AuthProvider { /// diff --git a/src/KubernetesClient/KubeConfigModels/Cluster.cs b/src/KubernetesClient/KubeConfigModels/Cluster.cs index 80faf96a5..4d0a81572 100644 --- a/src/KubernetesClient/KubeConfigModels/Cluster.cs +++ b/src/KubernetesClient/KubeConfigModels/Cluster.cs @@ -5,7 +5,6 @@ namespace k8s.KubeConfigModels /// /// Relates nicknames to cluster information. /// - [YamlSerializable] public class Cluster { /// diff --git a/src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs b/src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs index 06f8e1016..cdd791b4a 100644 --- a/src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs +++ b/src/KubernetesClient/KubeConfigModels/ClusterEndpoint.cs @@ -5,7 +5,6 @@ namespace k8s.KubeConfigModels /// /// Contains information about how to communicate with a kubernetes cluster /// - [YamlSerializable] public class ClusterEndpoint { /// @@ -39,10 +38,10 @@ public class ClusterEndpoint [YamlMember(Alias = "insecure-skip-tls-verify", ApplyNamingConventions = false)] public bool SkipTlsVerify { get; set; } - // /// - // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - // /// - // [YamlMember(Alias = "extensions")] - // public List Extensions { get; set; } + /// + /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + /// + [YamlMember(Alias = "extensions")] + public IEnumerable Extensions { get; set; } } } diff --git a/src/KubernetesClient/KubeConfigModels/Context.cs b/src/KubernetesClient/KubeConfigModels/Context.cs index 7834bbbdb..931a9fa42 100644 --- a/src/KubernetesClient/KubeConfigModels/Context.cs +++ b/src/KubernetesClient/KubeConfigModels/Context.cs @@ -5,7 +5,6 @@ namespace k8s.KubeConfigModels /// /// Relates nicknames to context information. /// - [YamlSerializable] public class Context { /// @@ -20,11 +19,11 @@ public class Context [YamlMember(Alias = "name")] public string Name { get; set; } - // /// - // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - // /// - // [YamlMember(Alias = "extensions")] - // public List Extensions { get; set; } + /// + /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + /// + [YamlMember(Alias = "extensions")] + public IEnumerable Extensions { get; set; } [Obsolete("This property is not set by the YAML config. Use ContextDetails.Namespace instead.")] diff --git a/src/KubernetesClient/KubeConfigModels/ContextDetails.cs b/src/KubernetesClient/KubeConfigModels/ContextDetails.cs index 6cbfd0073..4bae5c3fd 100644 --- a/src/KubernetesClient/KubeConfigModels/ContextDetails.cs +++ b/src/KubernetesClient/KubeConfigModels/ContextDetails.cs @@ -6,7 +6,6 @@ namespace k8s.KubeConfigModels /// Represents a tuple of references to a cluster (how do I communicate with a kubernetes cluster), /// a user (how do I identify myself), and a namespace (what subset of resources do I want to work with) /// - [YamlSerializable] public class ContextDetails { /// @@ -27,10 +26,10 @@ public class ContextDetails [YamlMember(Alias = "namespace")] public string Namespace { get; set; } - // /// - // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - // /// - // [YamlMember(Alias = "extensions")] - // public List Extensions { get; set; } + /// + /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + /// + [YamlMember(Alias = "extensions")] + public IEnumerable Extensions { get; set; } } } diff --git a/src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs b/src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs index 5bac4af5e..6654ba3bd 100644 --- a/src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs +++ b/src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs @@ -1,8 +1,5 @@ -using YamlDotNet.Serialization; - namespace k8s.KubeConfigModels { - [YamlSerializable] public class ExecCredentialResponse { public class ExecStatus diff --git a/src/KubernetesClient/KubeConfigModels/ExternalExecution.cs b/src/KubernetesClient/KubeConfigModels/ExternalExecution.cs index 7e18f449e..c3be35f04 100644 --- a/src/KubernetesClient/KubeConfigModels/ExternalExecution.cs +++ b/src/KubernetesClient/KubeConfigModels/ExternalExecution.cs @@ -2,7 +2,6 @@ namespace k8s.KubeConfigModels { - [YamlSerializable] public class ExternalExecution { [YamlMember(Alias = "apiVersion")] diff --git a/src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs b/src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs index a0c3a4fef..5308c209e 100644 --- a/src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs +++ b/src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs @@ -10,14 +10,13 @@ namespace k8s.KubeConfigModels /// Should be kept in sync with https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go /// Should update MergeKubeConfig in KubernetesClientConfiguration.ConfigFile.cs if updated. /// - [YamlSerializable] public class K8SConfiguration { - // /// - // /// Gets or sets general information to be use for CLI interactions - // /// - // [YamlMember(Alias = "preferences")] - // public IDictionary Preferences { get; set; } + /// + /// Gets or sets general information to be use for CLI interactions + /// + [YamlMember(Alias = "preferences")] + public IDictionary Preferences { get; set; } [YamlMember(Alias = "apiVersion")] public string ApiVersion { get; set; } @@ -35,25 +34,25 @@ public class K8SConfiguration /// Gets or sets a map of referencable names to context configs. /// [YamlMember(Alias = "contexts")] - public List Contexts { get; set; } = new List(); + public IEnumerable Contexts { get; set; } = new Context[0]; /// /// Gets or sets a map of referencable names to cluster configs. /// [YamlMember(Alias = "clusters")] - public List Clusters { get; set; } = new List(); + public IEnumerable Clusters { get; set; } = new Cluster[0]; /// /// Gets or sets a map of referencable names to user configs /// [YamlMember(Alias = "users")] - public List Users { get; set; } = new List(); + public IEnumerable Users { get; set; } = new User[0]; - // /// - // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - // /// - // [YamlMember(Alias = "extensions")] - // public List Extensions { get; set; } + /// + /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + /// + [YamlMember(Alias = "extensions")] + public IEnumerable Extensions { get; set; } /// /// Gets or sets the name of the Kubernetes configuration file. This property is set only when the configuration diff --git a/src/KubernetesClient/KubeConfigModels/NamedExtension.cs b/src/KubernetesClient/KubeConfigModels/NamedExtension.cs index 07d6eda32..676197975 100644 --- a/src/KubernetesClient/KubeConfigModels/NamedExtension.cs +++ b/src/KubernetesClient/KubeConfigModels/NamedExtension.cs @@ -1,22 +1,22 @@ +using YamlDotNet.Serialization; namespace k8s.KubeConfigModels { - // /// - // /// relates nicknames to extension information - // /// - // [YamlSerializable] - // public class NamedExtension - // { - // /// - // /// Gets or sets the nickname for this extension. - // /// - // [YamlMember(Alias = "name")] - // public string Name { get; set; } + /// + /// relates nicknames to extension information + /// + public class NamedExtension + { + /// + /// Gets or sets the nickname for this extension. + /// + [YamlMember(Alias = "name")] + public string Name { get; set; } - // /// - // /// Get or sets the extension information. - // /// - // [YamlMember(Alias = "extension")] - // public dynamic Extension { get; set; } - // } + /// + /// Get or sets the extension information. + /// + [YamlMember(Alias = "extension")] + public dynamic Extension { get; set; } + } } diff --git a/src/KubernetesClient/KubeConfigModels/User.cs b/src/KubernetesClient/KubeConfigModels/User.cs index 557f02256..b92d7c564 100644 --- a/src/KubernetesClient/KubeConfigModels/User.cs +++ b/src/KubernetesClient/KubeConfigModels/User.cs @@ -5,7 +5,6 @@ namespace k8s.KubeConfigModels /// /// Relates nicknames to auth information. /// - [YamlSerializable] public class User { /// diff --git a/src/KubernetesClient/KubeConfigModels/UserCredentials.cs b/src/KubernetesClient/KubeConfigModels/UserCredentials.cs index 8ef46fc95..b5dfc8a5a 100644 --- a/src/KubernetesClient/KubeConfigModels/UserCredentials.cs +++ b/src/KubernetesClient/KubeConfigModels/UserCredentials.cs @@ -5,7 +5,6 @@ namespace k8s.KubeConfigModels /// /// Contains information that describes identity information. This is use to tell the kubernetes cluster who you are. /// - [YamlSerializable] public class UserCredentials { /// @@ -74,11 +73,11 @@ public class UserCredentials [YamlMember(Alias = "auth-provider", ApplyNamingConventions = false)] public AuthProvider AuthProvider { get; set; } - // /// - // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - // /// - // [YamlMember(Alias = "extensions")] - // public IEnumerable Extensions { get; set; } + /// + /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. + /// + [YamlMember(Alias = "extensions")] + public IEnumerable Extensions { get; set; } /// /// Gets or sets external command and its arguments to receive user credentials diff --git a/src/KubernetesClient/KubernetesClient.csproj b/src/KubernetesClient/KubernetesClient.csproj index a95781491..bedbc9ff9 100644 --- a/src/KubernetesClient/KubernetesClient.csproj +++ b/src/KubernetesClient/KubernetesClient.csproj @@ -3,8 +3,9 @@ net6.0;net7.0;net8.0 k8s - true - true + + true @@ -14,7 +15,6 @@ - diff --git a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs index da0284b72..e74660bc9 100644 --- a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs +++ b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs @@ -459,16 +459,16 @@ public static Process CreateRunnableExternalProcess(ExternalExecution config, Ev throw new ArgumentNullException(nameof(config)); } - // var execInfo = new Dictionary - // { - // { "apiVersion", config.ApiVersion }, - // { "kind", "ExecCredentials" }, - // { "spec", new Dictionary { { "interactive", Environment.UserInteractive } } }, - // }; + var execInfo = new Dictionary + { + { "apiVersion", config.ApiVersion }, + { "kind", "ExecCredentials" }, + { "spec", new Dictionary { { "interactive", Environment.UserInteractive } } }, + }; var process = new Process(); - process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", $"{{ \"apiVersion\":\"{config.ApiVersion}\",\"kind\":\"ExecCredentials\",\"spec\":{{ \"interactive\":{Environment.UserInteractive.ToString().ToLower()} }} }}"); + process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", JsonSerializer.Serialize(execInfo)); if (config.EnvironmentVariables != null) { foreach (var configEnvironmentVariable in config.EnvironmentVariables) @@ -753,23 +753,23 @@ private static void MergeKubeConfig(K8SConfiguration basek8SConfig, K8SConfigura $"kubeconfig \"kind\" are different between {basek8SConfig.FileName} and {mergek8SConfig.FileName}"); } - // if (mergek8SConfig.Preferences != null) - // { - // foreach (var preference in mergek8SConfig.Preferences) - // { - // if (basek8SConfig.Preferences?.ContainsKey(preference.Key) == false) - // { - // basek8SConfig.Preferences[preference.Key] = preference.Value; - // } - // } - // } + if (mergek8SConfig.Preferences != null) + { + foreach (var preference in mergek8SConfig.Preferences) + { + if (basek8SConfig.Preferences?.ContainsKey(preference.Key) == false) + { + basek8SConfig.Preferences[preference.Key] = preference.Value; + } + } + } // Note, Clusters, Contexts, and Extensions are map-like in config despite being represented as a list here: // https://github.com/kubernetes/client-go/blob/ede92e0fe62deed512d9ceb8bf4186db9f3776ff/tools/clientcmd/api/types.go#L238 - // basek8SConfig.Extensions = MergeLists(basek8SConfig.Extensions, mergek8SConfig.Extensions, (s) => s.Name).ToList(); - basek8SConfig.Clusters = MergeLists(basek8SConfig.Clusters, mergek8SConfig.Clusters, (s) => s.Name).ToList(); - basek8SConfig.Users = MergeLists(basek8SConfig.Users, mergek8SConfig.Users, (s) => s.Name).ToList(); - basek8SConfig.Contexts = MergeLists(basek8SConfig.Contexts, mergek8SConfig.Contexts, (s) => s.Name).ToList(); + basek8SConfig.Extensions = MergeLists(basek8SConfig.Extensions, mergek8SConfig.Extensions, (s) => s.Name); + basek8SConfig.Clusters = MergeLists(basek8SConfig.Clusters, mergek8SConfig.Clusters, (s) => s.Name); + basek8SConfig.Users = MergeLists(basek8SConfig.Users, mergek8SConfig.Users, (s) => s.Name); + basek8SConfig.Contexts = MergeLists(basek8SConfig.Contexts, mergek8SConfig.Contexts, (s) => s.Name); } private static IEnumerable MergeLists(IEnumerable baseList, IEnumerable mergeList, diff --git a/src/KubernetesClient/KubernetesJson.cs b/src/KubernetesClient/KubernetesJson.cs index 2450b7d9d..d0d4e9d13 100644 --- a/src/KubernetesClient/KubernetesJson.cs +++ b/src/KubernetesClient/KubernetesJson.cs @@ -78,10 +78,6 @@ static KubernetesJson() JsonSerializerOptions.Converters.Add(new KubernetesDateTimeOffsetConverter()); JsonSerializerOptions.Converters.Add(new V1Status.V1StatusObjectViewConverter()); JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - -#if NET8_0_OR_GREATER - JsonSerializerOptions.TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? new System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver() : SourceGenerationContext.Default; -#endif } /// diff --git a/src/KubernetesClient/KubernetesYaml.cs b/src/KubernetesClient/KubernetesYaml.cs index 8d89874b2..b40ed8d44 100644 --- a/src/KubernetesClient/KubernetesYaml.cs +++ b/src/KubernetesClient/KubernetesYaml.cs @@ -12,15 +12,14 @@ namespace k8s /// public static class KubernetesYaml { - private static StaticDeserializerBuilder CommonDeserializerBuilder => - new StaticDeserializerBuilder(new k8s.KubeConfigModels.StaticContext()) + private static DeserializerBuilder CommonDeserializerBuilder => + new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .WithTypeConverter(new IntOrStringYamlConverter()) .WithTypeConverter(new ByteArrayStringYamlConverter()) .WithTypeConverter(new ResourceQuantityYamlConverter()) .WithAttemptingUnquotedStringTypeDeserialization() - // .WithOverridesFromJsonPropertyAttributes() - ; + .WithOverridesFromJsonPropertyAttributes(); private static readonly IDeserializer StrictDeserializer = CommonDeserializerBuilder From e9a9aabe3d5b27360b19cd37c21e5d0a31153787 Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Sat, 13 Jan 2024 03:20:07 -0800 Subject: [PATCH 13/19] Remove commented out code --- .../KubeConfigModels/ClusterEndpoint.cs | 6 ------ src/KubernetesClient.Aot/KubeConfigModels/ContextDetails.cs | 6 ------ .../KubeConfigModels/UserCredentials.cs | 6 ------ 3 files changed, 18 deletions(-) diff --git a/src/KubernetesClient.Aot/KubeConfigModels/ClusterEndpoint.cs b/src/KubernetesClient.Aot/KubeConfigModels/ClusterEndpoint.cs index 06f8e1016..c40827651 100644 --- a/src/KubernetesClient.Aot/KubeConfigModels/ClusterEndpoint.cs +++ b/src/KubernetesClient.Aot/KubeConfigModels/ClusterEndpoint.cs @@ -38,11 +38,5 @@ public class ClusterEndpoint /// [YamlMember(Alias = "insecure-skip-tls-verify", ApplyNamingConventions = false)] public bool SkipTlsVerify { get; set; } - - // /// - // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - // /// - // [YamlMember(Alias = "extensions")] - // public List Extensions { get; set; } } } diff --git a/src/KubernetesClient.Aot/KubeConfigModels/ContextDetails.cs b/src/KubernetesClient.Aot/KubeConfigModels/ContextDetails.cs index 6cbfd0073..ca2bf1e07 100644 --- a/src/KubernetesClient.Aot/KubeConfigModels/ContextDetails.cs +++ b/src/KubernetesClient.Aot/KubeConfigModels/ContextDetails.cs @@ -26,11 +26,5 @@ public class ContextDetails /// [YamlMember(Alias = "namespace")] public string Namespace { get; set; } - - // /// - // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - // /// - // [YamlMember(Alias = "extensions")] - // public List Extensions { get; set; } } } diff --git a/src/KubernetesClient.Aot/KubeConfigModels/UserCredentials.cs b/src/KubernetesClient.Aot/KubeConfigModels/UserCredentials.cs index 8ef46fc95..bd8a5063e 100644 --- a/src/KubernetesClient.Aot/KubeConfigModels/UserCredentials.cs +++ b/src/KubernetesClient.Aot/KubeConfigModels/UserCredentials.cs @@ -74,12 +74,6 @@ public class UserCredentials [YamlMember(Alias = "auth-provider", ApplyNamingConventions = false)] public AuthProvider AuthProvider { get; set; } - // /// - // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields. - // /// - // [YamlMember(Alias = "extensions")] - // public IEnumerable Extensions { get; set; } - /// /// Gets or sets external command and its arguments to receive user credentials /// From e999759da064e8889293a4575920722d400a5ee8 Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Sun, 14 Jan 2024 03:41:41 -0800 Subject: [PATCH 14/19] eliminate know warnings --- .../KubernetesClient.Aot.csproj | 3 - src/KubernetesClient.Aot/KubernetesJson.cs | 38 ++------ src/KubernetesClient.Aot/KubernetesYaml.cs | 95 ------------------- .../SourceGenerationContext.cs | 12 +++ .../V1PatchJsonConverter.cs | 27 ++++++ src/KubernetesClient/Watcher.cs | 1 + src/LibKubernetesGenerator/TypeHelper.cs | 14 +++ .../templates/Model.cs.template | 1 + 8 files changed, 64 insertions(+), 127 deletions(-) create mode 100644 src/KubernetesClient.Aot/SourceGenerationContext.cs create mode 100644 src/KubernetesClient.Aot/V1PatchJsonConverter.cs diff --git a/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj b/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj index cb04b85b2..07c300b39 100644 --- a/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj +++ b/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj @@ -6,7 +6,6 @@ true true true - AOT_ENABLED @@ -43,10 +42,8 @@ - - diff --git a/src/KubernetesClient.Aot/KubernetesJson.cs b/src/KubernetesClient.Aot/KubernetesJson.cs index d7c0b3062..cfa69ec01 100644 --- a/src/KubernetesClient.Aot/KubernetesJson.cs +++ b/src/KubernetesClient.Aot/KubernetesJson.cs @@ -6,9 +6,7 @@ namespace k8s { internal static class KubernetesJson { - private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions(); - - private sealed class Iso8601TimeSpanConverter : JsonConverter + internal sealed class Iso8601TimeSpanConverter : JsonConverter { public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -23,7 +21,7 @@ public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializer } } - private sealed class KubernetesDateTimeOffsetConverter : JsonConverter + internal sealed class KubernetesDateTimeOffsetConverter : JsonConverter { private const string RFC3339MicroFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.ffffffK"; private const string RFC3339NanoFormat = "yyyy-MM-dd'T'HH':'mm':'ss.fffffffK"; @@ -55,7 +53,7 @@ public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSeri } } - private sealed class KubernetesDateTimeConverter : JsonConverter + internal sealed class KubernetesDateTimeConverter : JsonConverter { private static readonly JsonConverter UtcConverter = new KubernetesDateTimeOffsetConverter(); public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -69,21 +67,6 @@ public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializer } } - static KubernetesJson() - { - 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()); - -#if NET8_0_OR_GREATER - JsonSerializerOptions.TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? new System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver() : SourceGenerationContext.Default; -#endif - } - /// /// Configures for the . /// To override existing converters, add them to the top of the list @@ -92,27 +75,24 @@ static KubernetesJson() /// An to configure the . public static void AddJsonOptions(Action configure) { - if (configure is null) - { - throw new ArgumentNullException(nameof(configure)); - } - - configure(JsonSerializerOptions); } public static TValue Deserialize(string json, JsonSerializerOptions jsonSerializerOptions = null) { - return JsonSerializer.Deserialize(json, jsonSerializerOptions ?? JsonSerializerOptions); + var info = SourceGenerationContext.Default.GetTypeInfo(typeof(TValue)); + return (TValue)JsonSerializer.Deserialize(json, info); } public static TValue Deserialize(Stream json, JsonSerializerOptions jsonSerializerOptions = null) { - return JsonSerializer.Deserialize(json, jsonSerializerOptions ?? JsonSerializerOptions); + var info = SourceGenerationContext.Default.GetTypeInfo(typeof(TValue)); + return (TValue)JsonSerializer.Deserialize(json, info); } public static string Serialize(object value, JsonSerializerOptions jsonSerializerOptions = null) { - return JsonSerializer.Serialize(value, jsonSerializerOptions ?? JsonSerializerOptions); + var info = SourceGenerationContext.Default.GetTypeInfo(value.GetType()); + return JsonSerializer.Serialize(value, info); } } } diff --git a/src/KubernetesClient.Aot/KubernetesYaml.cs b/src/KubernetesClient.Aot/KubernetesYaml.cs index 2ae6405c6..a017d1050 100644 --- a/src/KubernetesClient.Aot/KubernetesYaml.cs +++ b/src/KubernetesClient.Aot/KubernetesYaml.cs @@ -1,4 +1,3 @@ -using System.Reflection; using System.Text; using YamlDotNet.Core; using YamlDotNet.Core.Events; @@ -39,19 +38,6 @@ internal static class KubernetesYaml .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) .BuildValueSerializer(); - private static readonly IDictionary ModelTypeMap = typeof(KubernetesEntityAttribute).Assembly - .GetTypes() - .Where(t => t.GetCustomAttributes(typeof(KubernetesEntityAttribute), true).Any()) - .ToDictionary( - t => - { - var attr = (KubernetesEntityAttribute)t.GetCustomAttribute( - typeof(KubernetesEntityAttribute), true); - var groupPrefix = string.IsNullOrEmpty(attr.Group) ? "" : $"{attr.Group}/"; - return $"{groupPrefix}{attr.ApiVersion}/{attr.Kind}"; - }, - t => t); - private class ByteArrayStringYamlConverter : IYamlTypeConverter { public bool Accepts(Type type) @@ -88,87 +74,6 @@ public void WriteYaml(IEmitter emitter, object value, Type type) } } - /// - /// Load a collection of objects from a stream asynchronously - /// - /// caller is responsible for closing the stream - /// - /// - /// The stream to load the objects from. - /// - /// - /// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will - /// be used. - /// - /// true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise - /// collection of objects - public static async Task> LoadAllFromStreamAsync(Stream stream, IDictionary typeMap = null, bool strict = false) - { - var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync().ConfigureAwait(false); - return LoadAllFromString(content, typeMap); - } - - - /// - /// Load a collection of objects from a file asynchronously - /// - /// The name of the file to load from. - /// - /// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will - /// be used. - /// - /// true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise - /// collection of objects - public static async Task> LoadAllFromFileAsync(string fileName, IDictionary typeMap = null, bool strict = false) - { - using (var fileStream = File.OpenRead(fileName)) - { - return await LoadAllFromStreamAsync(fileStream, typeMap).ConfigureAwait(false); - } - } - - /// - /// Load a collection of objects from a string - /// - /// - /// The string to load the objects from. - /// - /// - /// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will - /// be used. - /// - /// true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise - /// collection of objects - public static List LoadAllFromString(string content, IDictionary typeMap = null, bool strict = false) - { - var mergedTypeMap = new Dictionary(ModelTypeMap); - // merge in KVPs from typeMap, overriding any in ModelTypeMap - typeMap?.ToList().ForEach(x => mergedTypeMap[x.Key] = x.Value); - - var types = new List(); - var parser = new MergingParser(new Parser(new StringReader(content))); - parser.Consume(); - while (parser.Accept(out _)) - { - var dict = GetDeserializer(strict).Deserialize>(parser); - types.Add(mergedTypeMap[dict["apiVersion"] + "/" + dict["kind"]]); - } - - parser = new MergingParser(new Parser(new StringReader(content))); - parser.Consume(); - var ix = 0; - var results = new List(); - while (parser.Accept(out _)) - { - var objType = types[ix++]; - var obj = GetDeserializer(strict).Deserialize(parser, objType); - results.Add(obj); - } - - return results; - } - public static async Task LoadFromStreamAsync(Stream stream, bool strict = false) { var reader = new StreamReader(stream); diff --git a/src/KubernetesClient.Aot/SourceGenerationContext.cs b/src/KubernetesClient.Aot/SourceGenerationContext.cs new file mode 100644 index 000000000..93912aba4 --- /dev/null +++ b/src/KubernetesClient.Aot/SourceGenerationContext.cs @@ -0,0 +1,12 @@ +using static k8s.KubernetesJson; + +namespace k8s; + +[JsonSourceGenerationOptions( + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + Converters = new[] { typeof(Iso8601TimeSpanConverter), typeof(KubernetesDateTimeConverter), typeof(KubernetesDateTimeOffsetConverter), typeof(JsonStringEnumConverter) }) + ] +internal partial class SourceGenerationContext : JsonSerializerContext +{ +} diff --git a/src/KubernetesClient.Aot/V1PatchJsonConverter.cs b/src/KubernetesClient.Aot/V1PatchJsonConverter.cs new file mode 100644 index 000000000..2d9df4639 --- /dev/null +++ b/src/KubernetesClient.Aot/V1PatchJsonConverter.cs @@ -0,0 +1,27 @@ +namespace k8s.Models +{ + internal sealed class V1PatchJsonConverter : JsonConverter + { + public override V1Patch Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, V1Patch value, JsonSerializerOptions options) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + var content = value?.Content; + if (content is string s) + { + writer.WriteRawValue(s); + return; + } + + throw new NotSupportedException("only string json patch is supported"); + } + } +} diff --git a/src/KubernetesClient/Watcher.cs b/src/KubernetesClient/Watcher.cs index 9546fa21b..882f33679 100644 --- a/src/KubernetesClient/Watcher.cs +++ b/src/KubernetesClient/Watcher.cs @@ -4,6 +4,7 @@ namespace k8s { /// Describes the type of a watch event. + [JsonConverter(typeof(JsonStringEnumConverter))] public enum WatchEventType { /// Emitted when an object is created, modified to match a watch's filter, or when a watch is first opened. diff --git a/src/LibKubernetesGenerator/TypeHelper.cs b/src/LibKubernetesGenerator/TypeHelper.cs index 0f38b5bf5..474f472dd 100644 --- a/src/LibKubernetesGenerator/TypeHelper.cs +++ b/src/LibKubernetesGenerator/TypeHelper.cs @@ -19,6 +19,7 @@ public TypeHelper(ClassNameHelper classNameHelper) public void RegisterHelper() { Helpers.Register(nameof(GetDotNetType), GetDotNetType); + Helpers.Register(nameof(GetFieldAnnotation), GetFieldAnnotation); Helpers.Register(nameof(GetReturnType), GetReturnType); Helpers.Register(nameof(IfReturnType), IfReturnType); Helpers.Register(nameof(IfType), IfType); @@ -204,6 +205,19 @@ public string GetDotNetType(JsonSchemaProperty p) return GetDotNetType(p.Type, p.Name, p.IsRequired, p.Format); } + public void GetFieldAnnotation(RenderContext context, IList arguments, + IDictionary options, + RenderBlock fn, RenderBlock inverse) + { + var property = arguments[0] as JsonSchemaProperty; + var nettype = GetDotNetType(property); + + if (nettype.StartsWith("System.DateTime")) + { + context.Write("[JsonConverter(typeof(KubernetesJson.KubernetesDateTimeConverter))]"); + } + } + public void GetReturnType(RenderContext context, IList arguments, IDictionary options, RenderBlock fn, RenderBlock inverse) diff --git a/src/LibKubernetesGenerator/templates/Model.cs.template b/src/LibKubernetesGenerator/templates/Model.cs.template index 836940db2..049bf16ce 100644 --- a/src/LibKubernetesGenerator/templates/Model.cs.template +++ b/src/LibKubernetesGenerator/templates/Model.cs.template @@ -54,6 +54,7 @@ namespace k8s.Models /// {{ToXmlDoc description}} /// [JsonPropertyName("{{name}}")] + {{GetFieldAnnotation .}} public {{GetDotNetType .}} {{GetDotNetName name "field"}} { get; set; } {{/properties}} From fb6e396cad062ca2eb7f4b6e6888beb8f6f0e150 Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Sun, 14 Jan 2024 04:18:39 -0800 Subject: [PATCH 15/19] add e2e test for aot --- .../KubernetesClient.Aot.csproj | 4 +- .../SourceGenerationContext.cs | 2 +- src/KubernetesClient/Watcher.cs | 2 + tests/E2E.Aot.Tests/E2E.Aot.Tests.csproj | 37 ++ tests/E2E.Aot.Tests/MinikubeTests.cs | 383 ++++++++++++++++++ tests/E2E.Tests/E2E.Tests.csproj | 1 - 6 files changed, 425 insertions(+), 4 deletions(-) create mode 100644 tests/E2E.Aot.Tests/E2E.Aot.Tests.csproj create mode 100644 tests/E2E.Aot.Tests/MinikubeTests.cs diff --git a/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj b/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj index 07c300b39..401c32469 100644 --- a/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj +++ b/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj @@ -84,8 +84,8 @@ - - + + diff --git a/src/KubernetesClient.Aot/SourceGenerationContext.cs b/src/KubernetesClient.Aot/SourceGenerationContext.cs index 93912aba4..decb9b5a9 100644 --- a/src/KubernetesClient.Aot/SourceGenerationContext.cs +++ b/src/KubernetesClient.Aot/SourceGenerationContext.cs @@ -5,7 +5,7 @@ namespace k8s; [JsonSourceGenerationOptions( DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, - Converters = new[] { typeof(Iso8601TimeSpanConverter), typeof(KubernetesDateTimeConverter), typeof(KubernetesDateTimeOffsetConverter), typeof(JsonStringEnumConverter) }) + Converters = new[] { typeof(Iso8601TimeSpanConverter), typeof(KubernetesDateTimeConverter), typeof(KubernetesDateTimeOffsetConverter) }) ] internal partial class SourceGenerationContext : JsonSerializerContext { diff --git a/src/KubernetesClient/Watcher.cs b/src/KubernetesClient/Watcher.cs index 882f33679..23868d4e0 100644 --- a/src/KubernetesClient/Watcher.cs +++ b/src/KubernetesClient/Watcher.cs @@ -4,7 +4,9 @@ namespace k8s { /// Describes the type of a watch event. +#if NET8_0_OR_GREATER [JsonConverter(typeof(JsonStringEnumConverter))] +#endif public enum WatchEventType { /// Emitted when an object is created, modified to match a watch's filter, or when a watch is first opened. diff --git a/tests/E2E.Aot.Tests/E2E.Aot.Tests.csproj b/tests/E2E.Aot.Tests/E2E.Aot.Tests.csproj new file mode 100644 index 000000000..13842028b --- /dev/null +++ b/tests/E2E.Aot.Tests/E2E.Aot.Tests.csproj @@ -0,0 +1,37 @@ + + + false + k8s.E2E + net8.0 + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + diff --git a/tests/E2E.Aot.Tests/MinikubeTests.cs b/tests/E2E.Aot.Tests/MinikubeTests.cs new file mode 100644 index 000000000..e74480d7d --- /dev/null +++ b/tests/E2E.Aot.Tests/MinikubeTests.cs @@ -0,0 +1,383 @@ +using ICSharpCode.SharpZipLib.Tar; +using Json.Patch; +using k8s.Autorest; +using k8s.Models; +using Nito.AsyncEx; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace k8s.E2E +{ + [Collection(nameof(Onebyone))] + public class MinikubeTests + { + [MinikubeFact] + public void SimpleTest() + { + var namespaceParameter = "default"; + var podName = "k8scsharp-e2e-pod"; + + using var client = CreateClient(); + + void Cleanup() + { + var pods = client.CoreV1.ListNamespacedPod(namespaceParameter); + while (pods.Items.Any(p => p.Metadata.Name == podName)) + { + try + { + client.CoreV1.DeleteNamespacedPod(podName, namespaceParameter); + } + catch (HttpOperationException e) + { + if (e.Response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return; + } + } + } + } + + try + { + Cleanup(); + + client.CoreV1.CreateNamespacedPod( + new V1Pod() + { + Metadata = new V1ObjectMeta { Name = podName, }, + Spec = new V1PodSpec + { + Containers = new[] { new V1Container() { Name = "k8scsharp-e2e", Image = "nginx", }, }, + }, + }, + namespaceParameter); + + var pods = client.CoreV1.ListNamespacedPod(namespaceParameter); + Assert.Contains(pods.Items, p => p.Metadata.Name == podName); + } + finally + { + Cleanup(); + } + } + + [MinikubeFact] + public async Task LogStreamTestAsync() + { + var namespaceParameter = "default"; + var podName = "k8scsharp-e2e-logstream-pod"; + + using var client = CreateClient(); + + void Cleanup() + { + var pods = client.CoreV1.ListNamespacedPod(namespaceParameter); + while (pods.Items.Any(p => p.Metadata.Name == podName)) + { + try + { + client.CoreV1.DeleteNamespacedPod(podName, namespaceParameter); + } + catch (HttpOperationException e) + { + if (e.Response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return; + } + } + } + } + + try + { + Cleanup(); + + client.CoreV1.CreateNamespacedPod( + new V1Pod() + { + Metadata = new V1ObjectMeta { Name = podName, }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container() + { + Name = "k8scsharp-e2e-logstream", + Image = "busybox", + Command = new[] { "ping" }, + Args = new[] { "-i", "10", "127.0.0.1" }, + }, + }, + }, + }, + namespaceParameter); + + var lines = new List(); + var started = new ManualResetEvent(false); + + async Task Pod() + { + var pods = client.CoreV1.ListNamespacedPod(namespaceParameter); + var pod = pods.Items.First(p => p.Metadata.Name == podName); + while (pod.Status.Phase != "Running") + { + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + return await Pod().ConfigureAwait(false); + } + + return pod; + } + + var pod = await Pod().ConfigureAwait(false); + var stream = client.CoreV1.ReadNamespacedPodLog(pod.Metadata.Name, pod.Metadata.NamespaceProperty, follow: true); + using var reader = new StreamReader(stream); + + var copytask = Task.Run(() => + { + for (; ; ) + { + try + { + lines.Add(reader.ReadLine()); + } + finally + { + started.Set(); + } + } + }); + + Assert.True(started.WaitOne(TimeSpan.FromMinutes(2))); + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + Assert.Null(copytask.Exception); + Assert.Equal(2, lines.Count); + await Task.Delay(TimeSpan.FromSeconds(11)).ConfigureAwait(false); + Assert.Equal(3, lines.Count); + } + finally + { + Cleanup(); + } + } + + [MinikubeFact] + public async Task DatetimeFieldTest() + { + 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() + { + 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); + } + + [MinikubeFact] + public async Task CopyToPodTestAsync() + { + var namespaceParameter = "default"; + var podName = "k8scsharp-e2e-cp-pod"; + + using var client = CreateClient(); + + async Task CopyFileToPodAsync(string name, string @namespace, string container, Stream inputFileStream, string destinationFilePath, CancellationToken cancellationToken = default(CancellationToken)) + { + // The callback which processes the standard input, standard output and standard error of exec method + var handler = new ExecAsyncCallback(async (stdIn, stdOut, stdError) => + { + var fileInfo = new FileInfo(destinationFilePath); + try + { + using (var memoryStream = new MemoryStream()) + { + using (var tarOutputStream = new TarOutputStream(memoryStream, Encoding.Default)) + { + tarOutputStream.IsStreamOwner = false; + + var fileSize = inputFileStream.Length; + var entry = TarEntry.CreateTarEntry(fileInfo.Name); + + entry.Size = fileSize; + + tarOutputStream.PutNextEntry(entry); + await inputFileStream.CopyToAsync(tarOutputStream).ConfigureAwait(false); + tarOutputStream.CloseEntry(); + } + + memoryStream.Position = 0; + + await memoryStream.CopyToAsync(stdIn).ConfigureAwait(false); + await memoryStream.FlushAsync().ConfigureAwait(false); + stdIn.Close(); + } + } + catch (Exception ex) + { + throw new IOException($"Copy command failed: {ex.Message}"); + } + + using StreamReader streamReader = new StreamReader(stdError); + while (streamReader.EndOfStream == false) + { + string error = await streamReader.ReadToEndAsync().ConfigureAwait(false); + throw new IOException($"Copy command failed: {error}"); + } + }); + + string destinationFolder = Path.GetDirectoryName(destinationFilePath).Replace("\\", "/"); + + return await client.NamespacedPodExecAsync( + name, + @namespace, + container, + new string[] { "tar", "-xmf", "-", "-C", destinationFolder }, + false, + handler, + cancellationToken).ConfigureAwait(false); + } + + + void Cleanup() + { + var pods = client.CoreV1.ListNamespacedPod(namespaceParameter); + while (pods.Items.Any(p => p.Metadata.Name == podName)) + { + try + { + client.CoreV1.DeleteNamespacedPod(podName, namespaceParameter); + } + catch (HttpOperationException e) + { + if (e.Response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return; + } + } + } + } + + try + { + Cleanup(); + + client.CoreV1.CreateNamespacedPod( + new V1Pod() + { + Metadata = new V1ObjectMeta { Name = podName, }, + Spec = new V1PodSpec + { + Containers = new[] + { + new V1Container() + { + Name = "container", + Image = "ubuntu", + // Image = "busybox", // TODO not work with busybox + Command = new[] { "sleep" }, + Args = new[] { "infinity" }, + }, + }, + }, + }, + namespaceParameter); + + var lines = new List(); + var started = new ManualResetEvent(false); + + async Task Pod() + { + var pods = client.CoreV1.ListNamespacedPod(namespaceParameter); + var pod = pods.Items.First(p => p.Metadata.Name == podName); + while (pod.Status.Phase != "Running") + { + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + return await Pod().ConfigureAwait(false); + } + + return pod; + } + + var pod = await Pod().ConfigureAwait(false); + + + async Task AssertMd5sumAsync(string file, byte[] orig) + { + var ws = await client.WebSocketNamespacedPodExecAsync( + pod.Metadata.Name, + pod.Metadata.NamespaceProperty, + new string[] { "md5sum", file }, + "container").ConfigureAwait(false); + + var demux = new StreamDemuxer(ws); + demux.Start(); + + var buff = new byte[4096]; + var stream = demux.GetStream(1, 1); + var read = stream.Read(buff, 0, 4096); + var remotemd5 = Encoding.Default.GetString(buff); + remotemd5 = remotemd5.Substring(0, 32); + + var md5 = MD5.Create().ComputeHash(orig); + var localmd5 = BitConverter.ToString(md5).Replace("-", string.Empty).ToLower(); + + Assert.Equal(localmd5, remotemd5); + } + + + // + { + // small + var content = new byte[1 * 1024 * 1024]; + new Random().NextBytes(content); + await CopyFileToPodAsync(pod.Metadata.Name, pod.Metadata.NamespaceProperty, "container", new MemoryStream(content), "/tmp/test").ConfigureAwait(false); + await AssertMd5sumAsync("/tmp/test", content).ConfigureAwait(false); + } + + { + // big + var content = new byte[40 * 1024 * 1024]; + new Random().NextBytes(content); + await CopyFileToPodAsync(pod.Metadata.Name, pod.Metadata.NamespaceProperty, "container", new MemoryStream(content), "/tmp/test").ConfigureAwait(false); + await AssertMd5sumAsync("/tmp/test", content).ConfigureAwait(false); + } + } + finally + { + Cleanup(); + } + } + + public static IKubernetes CreateClient() + { + return new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()); + } + } +} diff --git a/tests/E2E.Tests/E2E.Tests.csproj b/tests/E2E.Tests/E2E.Tests.csproj index 94935879a..6fc9b204e 100644 --- a/tests/E2E.Tests/E2E.Tests.csproj +++ b/tests/E2E.Tests/E2E.Tests.csproj @@ -1,7 +1,6 @@ false - true k8s.E2E net6.0;net7.0;net8.0 From d82d9a9a9bb9eda8aed3ebdc9a5c76821f20f85a Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Sun, 14 Jan 2024 04:23:46 -0800 Subject: [PATCH 16/19] rever on field convert annotation --- src/LibKubernetesGenerator/TypeHelper.cs | 14 -------------- .../templates/Model.cs.template | 1 - 2 files changed, 15 deletions(-) diff --git a/src/LibKubernetesGenerator/TypeHelper.cs b/src/LibKubernetesGenerator/TypeHelper.cs index 474f472dd..0f38b5bf5 100644 --- a/src/LibKubernetesGenerator/TypeHelper.cs +++ b/src/LibKubernetesGenerator/TypeHelper.cs @@ -19,7 +19,6 @@ public TypeHelper(ClassNameHelper classNameHelper) public void RegisterHelper() { Helpers.Register(nameof(GetDotNetType), GetDotNetType); - Helpers.Register(nameof(GetFieldAnnotation), GetFieldAnnotation); Helpers.Register(nameof(GetReturnType), GetReturnType); Helpers.Register(nameof(IfReturnType), IfReturnType); Helpers.Register(nameof(IfType), IfType); @@ -205,19 +204,6 @@ public string GetDotNetType(JsonSchemaProperty p) return GetDotNetType(p.Type, p.Name, p.IsRequired, p.Format); } - public void GetFieldAnnotation(RenderContext context, IList arguments, - IDictionary options, - RenderBlock fn, RenderBlock inverse) - { - var property = arguments[0] as JsonSchemaProperty; - var nettype = GetDotNetType(property); - - if (nettype.StartsWith("System.DateTime")) - { - context.Write("[JsonConverter(typeof(KubernetesJson.KubernetesDateTimeConverter))]"); - } - } - public void GetReturnType(RenderContext context, IList arguments, IDictionary options, RenderBlock fn, RenderBlock inverse) diff --git a/src/LibKubernetesGenerator/templates/Model.cs.template b/src/LibKubernetesGenerator/templates/Model.cs.template index 049bf16ce..836940db2 100644 --- a/src/LibKubernetesGenerator/templates/Model.cs.template +++ b/src/LibKubernetesGenerator/templates/Model.cs.template @@ -54,7 +54,6 @@ namespace k8s.Models /// {{ToXmlDoc description}} /// [JsonPropertyName("{{name}}")] - {{GetFieldAnnotation .}} public {{GetDotNetType .}} {{GetDotNetName name "field"}} { get; set; } {{/properties}} From 7c0c9309336504fb5d19df7a051a8eaf4d307f2b Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Sun, 14 Jan 2024 04:31:11 -0800 Subject: [PATCH 17/19] add e2e aot gh --- .github/workflows/buildtest.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/buildtest.yaml b/.github/workflows/buildtest.yaml index c8424b6f6..a34eb7f73 100644 --- a/.github/workflows/buildtest.yaml +++ b/.github/workflows/buildtest.yaml @@ -80,7 +80,16 @@ jobs: cat skip.log echo "CASES MUST NOT BE SKIPPED" exit 1 - fi + fi + - name: AOT Test + run: | + true > skip.log + env K8S_E2E_MINIKUBE=1 dotnet test tests/E2E.Aot.Tests --logger "SkipTestLogger;file=$PWD/skip.log" + if [ -s skip.log ]; then + cat skip.log + echo "CASES MUST NOT BE SKIPPED" + exit 1 + fi on: pull_request: From cd9fcf8ae6276d8ffee6b29d78c175e517efdd87 Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Sun, 14 Jan 2024 04:31:44 -0800 Subject: [PATCH 18/19] Add KubernetesClient.Aot project reference --- src/nuget.proj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nuget.proj b/src/nuget.proj index 1d04b6fb5..feec6cda8 100644 --- a/src/nuget.proj +++ b/src/nuget.proj @@ -2,6 +2,7 @@ + From cb8d33f76a09322a3492bf8b88359188beb7ef55 Mon Sep 17 00:00:00 2001 From: Boshi LIAN Date: Sun, 14 Jan 2024 04:41:06 -0800 Subject: [PATCH 19/19] move CA1812 rule violation to file --- kubernetes-client.ruleset | 2 +- src/KubernetesClient.Aot/V1PatchJsonConverter.cs | 2 ++ tests/E2E.Aot.Tests/MinikubeTests.cs | 5 ----- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/kubernetes-client.ruleset b/kubernetes-client.ruleset index 151975f56..8baea16b9 100644 --- a/kubernetes-client.ruleset +++ b/kubernetes-client.ruleset @@ -268,7 +268,7 @@ - + diff --git a/src/KubernetesClient.Aot/V1PatchJsonConverter.cs b/src/KubernetesClient.Aot/V1PatchJsonConverter.cs index 2d9df4639..314ef5694 100644 --- a/src/KubernetesClient.Aot/V1PatchJsonConverter.cs +++ b/src/KubernetesClient.Aot/V1PatchJsonConverter.cs @@ -1,6 +1,8 @@ namespace k8s.Models { +#pragma warning disable CA1812 // Avoid uninstantiated internal classes internal sealed class V1PatchJsonConverter : JsonConverter +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { public override V1Patch Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/tests/E2E.Aot.Tests/MinikubeTests.cs b/tests/E2E.Aot.Tests/MinikubeTests.cs index e74480d7d..2c4ca933b 100644 --- a/tests/E2E.Aot.Tests/MinikubeTests.cs +++ b/tests/E2E.Aot.Tests/MinikubeTests.cs @@ -1,17 +1,12 @@ using ICSharpCode.SharpZipLib.Tar; -using Json.Patch; using k8s.Autorest; using k8s.Models; -using Nito.AsyncEx; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Xunit;