From 7193936e4078ae199e20771fee40297610e8c983 Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Tue, 7 Nov 2017 20:23:27 +0800 Subject: [PATCH 1/4] watcher handler will auto patch to all ctor path --- ...netes.Auth.cs => Kubernetes.ConfigInit.cs} | 99 +++++++++++-------- 1 file changed, 60 insertions(+), 39 deletions(-) rename src/{Kubernetes.Auth.cs => Kubernetes.ConfigInit.cs} (57%) diff --git a/src/Kubernetes.Auth.cs b/src/Kubernetes.ConfigInit.cs similarity index 57% rename from src/Kubernetes.Auth.cs rename to src/Kubernetes.ConfigInit.cs index 987efad90..cb010628b 100644 --- a/src/Kubernetes.Auth.cs +++ b/src/Kubernetes.ConfigInit.cs @@ -1,38 +1,36 @@ using k8s.Models; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using k8s.Exceptions; +using Microsoft.Rest; namespace k8s { - using System; - using System.Diagnostics.CodeAnalysis; - using System.Net.Http; - using System.Net.Security; - using System.Security.Cryptography.X509Certificates; - using System.Threading.Tasks; - using k8s.Exceptions; - using Microsoft.Rest; - - public partial class Kubernetes : ServiceClient, IKubernetes + public partial class Kubernetes { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// - /// Optional. The delegating handlers to add to the http client pipeline. + /// Optional. The delegating handlers to add to the http client pipeline. + /// + /// + /// Optional. The delegating handlers to add to the http client pipeline. /// - public Kubernetes(KubernetesClientConfiguration config) + public Kubernetes(KubernetesClientConfiguration config, params DelegatingHandler[] handlers) : this(handlers) { - this.Initialize(); - - this.CaCert = config.SslCaCert; - this.BaseUri = new Uri(config.Host); - - var handler = new HttpClientHandler(); + CaCert = config.SslCaCert; + BaseUri = new Uri(config.Host); if (BaseUri.Scheme == "https") { if (config.SkipTlsVerify) { - handler.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + HttpClientHandler.ServerCertificateCustomValidationCallback = + (sender, certificate, chain, sslPolicyErrors) => true; } else { @@ -41,21 +39,47 @@ public Kubernetes(KubernetesClientConfiguration config) throw new KubeConfigException("a CA must be set when SkipTlsVerify === false"); } - handler.ServerCertificateCustomValidationCallback = CertificateValidationCallBack; + HttpClientHandler.ServerCertificateCustomValidationCallback = CertificateValidationCallBack; } } // set credentails for the kubernernet client - this.SetCredentials(config, handler); - this.InitializeHttpClient(handler, new DelegatingHandler[]{new WatcherDelegatingHandler()}); - - DeserializationSettings.Converters.Add(new V1Status.V1StatusObjectViewConverter()); + SetCredentials(config, HttpClientHandler); + } + + private X509Certificate2 CaCert { get; } + + partial void CustomInitialize() + { + AppendDelegatingHandler(); + DeserializationSettings.Converters.Add(new V1Status.V1StatusObjectViewConverter()); } - private X509Certificate2 CaCert { get; set; } + private void AppendDelegatingHandler() where T : DelegatingHandler, new() + { + var cur = FirstMessageHandler as DelegatingHandler; + + while (cur != null) + { + var next = cur.InnerHandler as DelegatingHandler; + + if (next == null) + { + // last one + // append watcher handler between to last handler + cur.InnerHandler = new T + { + InnerHandler = cur.InnerHandler + }; + break; + } + + cur = next; + } + } /// - /// Set credentials for the Client + /// Set credentials for the Client /// /// k8s client configuration /// http client handler for the rest client @@ -88,7 +112,7 @@ private void SetCredentials(KubernetesClientConfiguration config, HttpClientHand } /// - /// SSl Cert Validation Callback + /// SSl Cert Validation Callback /// /// sender /// client certificate @@ -97,10 +121,10 @@ private void SetCredentials(KubernetesClientConfiguration config, HttpClientHand /// true if valid cert [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Unused by design")] private bool CertificateValidationCallBack( - object sender, - X509Certificate certificate, - X509Chain chain, - SslPolicyErrors sslPolicyErrors) + object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors) { // If the certificate is a valid, signed certificate, return true. if (sslPolicyErrors == SslPolicyErrors.None) @@ -114,16 +138,13 @@ private bool CertificateValidationCallBack( chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // add all your extra certificate chain - chain.ChainPolicy.ExtraStore.Add(this.CaCert); + chain.ChainPolicy.ExtraStore.Add(CaCert); chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; - var isValid = chain.Build((X509Certificate2)certificate); + var isValid = chain.Build((X509Certificate2) certificate); return isValid; } - else - { - // In all other cases, return false. - return false; - } + // In all other cases, return false. + return false; } } } From e06dcc57dcbb59f34e7b6a66e7e211dc1049f846 Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Tue, 7 Nov 2017 20:42:31 +0800 Subject: [PATCH 2/4] testcases: ensure handlers work with watcher --- tests/WatchTests.cs | 58 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/WatchTests.cs b/tests/WatchTests.cs index 45e05bc7b..976a2a4eb 100644 --- a/tests/WatchTests.cs +++ b/tests/WatchTests.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using k8s.Exceptions; @@ -270,5 +271,62 @@ public void WatchServerDisconnect() Assert.False(watcher.Watching); Assert.IsType(exceptionCatched); } + + private class DummyHandler : DelegatingHandler + { + internal bool Called { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + Called = true; + return base.SendAsync(request, cancellationToken); + } + } + + [Fact] + public void TestWatchWithHandlers() + { + using (var server = new MockKubeApiServer(async httpContext => + { + await WriteStreamLine(httpContext, MockKubeApiServer.MockPodResponse); + await Task.Delay(TimeSpan.FromMilliseconds(100)); + + await WriteStreamLine(httpContext, MockAddedEventStreamLine); + await Task.Delay(TimeSpan.FromMilliseconds(100)); + + // make server alive, cannot set to int.max as of it would block response + await Task.Delay(TimeSpan.FromDays(1)); + return false; + })) + { + var handler1 = new DummyHandler(); + var handler2 = new DummyHandler(); + + var client = new Kubernetes(new KubernetesClientConfiguration + { + Host = server.Uri.ToString() + }, handler1, handler2); + + Assert.False(handler1.Called); + Assert.False(handler2.Called); + + var listTask = client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).Result; + + var events = new HashSet(); + + var watcher = listTask.Watch( + (type, item) => { events.Add(type); } + ); + + // wait server yields all events + Thread.Sleep(TimeSpan.FromMilliseconds(500)); + + Assert.Contains(WatchEventType.Added, events); + + Assert.True(handler1.Called); + Assert.True(handler2.Called); + } + } } } From 5cb18e93347d9b52b29d0bb7b458aeeef62d051b Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Tue, 7 Nov 2017 20:54:13 +0800 Subject: [PATCH 3/4] remove ctor api of KubernetesClientConfiguration, should use factory style --- ...ubernetesClientConfiguration.ConfigFile.cs | 15 +------ src/KubernetesClientConfiguration.cs | 39 +++++++------------ tests/CertUtilsTests.cs | 4 +- tests/KubernetesClientConfigurationTests.cs | 34 ++++++++-------- 4 files changed, 35 insertions(+), 57 deletions(-) diff --git a/src/KubernetesClientConfiguration.ConfigFile.cs b/src/KubernetesClientConfiguration.ConfigFile.cs index 1dc5b07de..802476494 100644 --- a/src/KubernetesClientConfiguration.ConfigFile.cs +++ b/src/KubernetesClientConfiguration.ConfigFile.cs @@ -24,17 +24,6 @@ public partial class KubernetesClientConfiguration ? Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), @".kube\config") : Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".kube/config"); - /// - /// Initializes a new instance of the class. - /// - /// kubeconfig file info - /// Context to use from kube config - public KubernetesClientConfiguration(FileInfo kubeconfig = null, string currentContext = null) - { - var k8SConfig = LoadKubeConfig(kubeconfig ?? new FileInfo(KubeConfigDefaultLocation)); - this.Initialize(k8SConfig, currentContext); - } - /// /// Initializes a new instance of the from config file /// @@ -60,7 +49,7 @@ public static KubernetesClientConfiguration BuildConfigFromConfigFile(FileInfo k var k8SConfig = LoadKubeConfig(kubeconfig); var k8SConfiguration = new KubernetesClientConfiguration(); - k8SConfiguration.Initialize(k8SConfig); + k8SConfiguration.Initialize(k8SConfig, currentContext); if (!string.IsNullOrWhiteSpace(masterUrl)) { @@ -200,7 +189,7 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext) /// /// Loads Kube Config /// - /// Kube config file contents + /// Kube config file contents /// Instance of the class private static K8SConfiguration LoadKubeConfig(FileInfo kubeconfig) { diff --git a/src/KubernetesClientConfiguration.cs b/src/KubernetesClientConfiguration.cs index de0aaa315..b053d51f8 100644 --- a/src/KubernetesClientConfiguration.cs +++ b/src/KubernetesClientConfiguration.cs @@ -1,78 +1,67 @@ +using System.Security.Cryptography.X509Certificates; + namespace k8s { - using System; - using System.IO; - using System.Linq; - using System.Security.Cryptography.X509Certificates; - using k8s.Exceptions; - using k8s.KubeConfigModels; - using YamlDotNet.Serialization; - using System.Runtime.InteropServices; - /// - /// Represents a set of kubernetes client configuration settings + /// Represents a set of kubernetes client configuration settings /// public partial class KubernetesClientConfiguration { - public KubernetesClientConfiguration() - { - } - /// - /// Gets Host + /// Gets Host /// public string Host { get; set; } /// - /// Gets SslCaCert + /// Gets SslCaCert /// public X509Certificate2 SslCaCert { get; set; } /// - /// Gets ClientCertificateData + /// Gets ClientCertificateData /// public string ClientCertificateData { get; set; } /// - /// Gets ClientCertificate Key + /// Gets ClientCertificate Key /// public string ClientCertificateKeyData { get; set; } /// - /// Gets ClientCertificate filename + /// Gets ClientCertificate filename /// public string ClientCertificateFilePath { get; set; } /// - /// Gets ClientCertificate Key filename + /// Gets ClientCertificate Key filename /// public string ClientKeyFilePath { get; set; } /// - /// Gets a value indicating whether to skip ssl server cert validation + /// Gets a value indicating whether to skip ssl server cert validation /// public bool SkipTlsVerify { get; set; } /// - /// Gets or sets the HTTP user agent. + /// Gets or sets the HTTP user agent. /// /// Http user agent. public string UserAgent { get; set; } /// - /// Gets or sets the username (HTTP basic authentication). + /// Gets or sets the username (HTTP basic authentication). /// /// The username. public string Username { get; set; } /// - /// Gets or sets the password (HTTP basic authentication). + /// Gets or sets the password (HTTP basic authentication). /// /// The password. public string Password { get; set; } /// - /// Gets or sets the access token for OAuth2 authentication. + /// Gets or sets the access token for OAuth2 authentication. /// /// The access token. public string AccessToken { get; set; } diff --git a/tests/CertUtilsTests.cs b/tests/CertUtilsTests.cs index 3b3b83e15..ce290a726 100644 --- a/tests/CertUtilsTests.cs +++ b/tests/CertUtilsTests.cs @@ -19,7 +19,7 @@ public class CertUtilsTests public void LoadFromFiles() { var fi = new FileInfo(kubeConfigFileName); - var cfg = new KubernetesClientConfiguration(fi, "federal-context"); + var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, "federal-context"); // Just validate that this doesn't throw and private key is non-null var cert = CertUtils.GeneratePfx(cfg); @@ -33,7 +33,7 @@ public void LoadFromFiles() public void LoadFromInlineData() { var fi = new FileInfo(kubeConfigFileName); - var cfg = new KubernetesClientConfiguration(fi, "victorian-context"); + var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, "victorian-context"); // Just validate that this doesn't throw and private key is non-null var cert = CertUtils.GeneratePfx(cfg); diff --git a/tests/KubernetesClientConfigurationTests.cs b/tests/KubernetesClientConfigurationTests.cs index d21b8476c..68b78322d 100755 --- a/tests/KubernetesClientConfigurationTests.cs +++ b/tests/KubernetesClientConfigurationTests.cs @@ -64,7 +64,7 @@ public static string readLine(string fileName) public void ConfigurationFileNotFound() { var fi = new FileInfo("/path/to/nowhere"); - Assert.Throws(() => new KubernetesClientConfiguration(fi)); + Assert.Throws(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi)); } /// @@ -73,7 +73,7 @@ public void ConfigurationFileNotFound() [Fact] public void DefaultConfigurationLoaded() { - var cfg = new KubernetesClientConfiguration(new FileInfo(kubeConfigFileName)); + var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(new FileInfo(kubeConfigFileName)); Assert.NotNull(cfg.Host); } @@ -86,7 +86,7 @@ public void DefaultConfigurationLoaded() public void ContextHost(string context, string host) { var fi = new FileInfo(kubeConfigFileName); - var cfg = new KubernetesClientConfiguration(fi, context); + var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, context); Assert.Equal(host, cfg.Host); } @@ -100,7 +100,7 @@ public void ContextHost(string context, string host) public void ContextUserToken(string context, string token) { var fi = new FileInfo(kubeConfigFileName); - var cfg = new KubernetesClientConfiguration(fi, context); + var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, context); Assert.Equal(context, cfg.CurrentContext); Assert.Null(cfg.Username); Assert.Equal(token, cfg.AccessToken); @@ -117,7 +117,7 @@ public void ContextUserToken(string context, string token) public void ContextCertificateTest(string context, string clientCert, string clientCertKey) { var fi = new FileInfo(kubeConfigFileName); - var cfg = new KubernetesClientConfiguration(fi, context); + var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, context); Assert.Equal(context, cfg.CurrentContext); Assert.Equal(cfg.ClientCertificateFilePath, clientCert); Assert.Equal(cfg.ClientKeyFilePath, clientCertKey); @@ -132,7 +132,7 @@ public void ContextCertificateTest(string context, string clientCert, string cli public void ClientDataTest(string context) { var fi = new FileInfo(kubeConfigFileName); - var cfg = new KubernetesClientConfiguration(fi, context); + var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, context); Assert.Equal(context, cfg.CurrentContext); Assert.NotNull(cfg.SslCaCert); Assert.Equal(readLine("assets/client-certificate-data.txt"), cfg.ClientCertificateData); @@ -147,7 +147,7 @@ public void ClientDataTest(string context) public void ContextNotFound() { var fi = new FileInfo(kubeConfigFileName); - Assert.Throws(() => new KubernetesClientConfiguration(fi, "context-not-found")); + Assert.Throws(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, "context-not-found")); } /// @@ -157,7 +157,7 @@ public void ContextNotFound() public void NoContexts() { var fi = new FileInfo(kubeConfigNoContexts); - Assert.Throws(() => new KubernetesClientConfiguration(fi)); + Assert.Throws(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi)); } /// @@ -167,7 +167,7 @@ public void NoContexts() public void NoContextsExplicit() { var fi = new FileInfo(kubeConfigNoContexts); - Assert.Throws(() => new KubernetesClientConfiguration(fi, "context")); + Assert.Throws(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, "context")); } /// @@ -177,7 +177,7 @@ public void NoContextsExplicit() public void UserPasswordAuthentication() { var fi = new FileInfo(kubeConfigUserPassword); - var cfg = new KubernetesClientConfiguration(fi); + var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi); Assert.Equal("admin", cfg.Username); Assert.Equal("secret", cfg.Password); } @@ -189,7 +189,7 @@ public void UserPasswordAuthentication() public void IncompleteUserCredentials() { var fi = new FileInfo(kubeConfigNoCredentials); - Assert.Throws(() => new KubernetesClientConfiguration(fi)); + Assert.Throws(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi)); } /// @@ -199,7 +199,7 @@ public void IncompleteUserCredentials() public void ServerNotFound() { var fi = new FileInfo(kubeConfigNoServer); - Assert.Throws(() => new KubernetesClientConfiguration(fi)); + Assert.Throws(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi)); } /// @@ -209,7 +209,7 @@ public void ServerNotFound() public void ClusterNotFound() { var fi = new FileInfo(kubeConfigNoCluster); - Assert.Throws(() => new KubernetesClientConfiguration(fi)); + Assert.Throws(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi)); } /// @@ -219,7 +219,7 @@ public void ClusterNotFound() public void ClusterNameMissmatch() { var fi = new FileInfo(kubeConfigClusterMissmatch); - Assert.Throws(() => new KubernetesClientConfiguration(fi)); + Assert.Throws(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi)); } /// @@ -229,7 +229,7 @@ public void ClusterNameMissmatch() public void CheckClusterTlsCorrectness() { var fi = new FileInfo(kubeConfigTlsNoSkipError); - Assert.Throws(() => new KubernetesClientConfiguration(fi)); + Assert.Throws(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi)); } /// @@ -239,7 +239,7 @@ public void CheckClusterTlsCorrectness() public void CheckClusterTlsSkipCorrectness() { var fi = new FileInfo(kubeConfigTlsSkip); - var cfg = new KubernetesClientConfiguration(fi); + var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi); Assert.NotNull(cfg.Host); Assert.Null(cfg.SslCaCert); Assert.True(cfg.SkipTlsVerify); @@ -251,7 +251,7 @@ public void CheckClusterTlsSkipCorrectness() // [Fact] // public void ListDefaultNamespacedPod() // { - // var k8sClientConfig = new KubernetesClientConfiguration(); + // var k8sClientConfig = KubernetesClientConfiguration.BuildConfigFromConfigFile(); // IKubernetes client = new Kubernetes(k8sClientConfig); // var listTask = client.ListNamespacedPodWithHttpMessagesAsync("default").Result; // var list = listTask.Body; From 83559d58542bb74f5cd36ff3285a6ccbc7420d97 Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Sat, 11 Nov 2017 03:50:30 +0800 Subject: [PATCH 4/4] clean up test case naming --- tests/KubernetesClientConfigurationTests.cs | 2 +- tests/V1StatusObjectViewTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/KubernetesClientConfigurationTests.cs b/tests/KubernetesClientConfigurationTests.cs index 68b78322d..62d24e7a1 100755 --- a/tests/KubernetesClientConfigurationTests.cs +++ b/tests/KubernetesClientConfigurationTests.cs @@ -129,7 +129,7 @@ public void ContextCertificateTest(string context, string clientCert, string cli /// Context to retreive the configuration [Theory] [InlineData("victorian-context")] - public void ClientDataTest(string context) + public void ClientData(string context) { var fi = new FileInfo(kubeConfigFileName); var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, context); diff --git a/tests/V1StatusObjectViewTests.cs b/tests/V1StatusObjectViewTests.cs index 9874e9509..4b115b309 100644 --- a/tests/V1StatusObjectViewTests.cs +++ b/tests/V1StatusObjectViewTests.cs @@ -8,7 +8,7 @@ namespace k8s.Tests public class V1StatusObjectViewTests { [Fact] - public void TestReturnStatus() + public void ReturnStatus() { var v1Status = new V1Status { @@ -32,7 +32,7 @@ public void TestReturnStatus() } [Fact] - public void TestReturnObject() + public void ReturnObject() { var corev1Namespace = new Corev1Namespace() {