Skip to content
49 changes: 49 additions & 0 deletions src/KubernetesClient/Fluent/Kubernetes.Fluent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Net.Http;
using k8s.Models;

namespace k8s.Fluent
{
public static class KubernetesFluent
{
/// <summary>Creates a new Kubernetes object of the given type and sets its <see cref="IKubernetesObject.ApiVersion"/> and
/// <see cref="IKubernetesObject.Kind"/>.
/// </summary>
public static T New<T>(this Kubernetes client) where T : IKubernetesObject, new() => client.Scheme.New<T>();

/// <summary>Creates a new Kubernetes object of the given type and sets its <see cref="IKubernetesObject.ApiVersion"/>,
/// <see cref="IKubernetesObject.Kind"/>, and <see cref="V1ObjectMeta.Name"/>.
/// </summary>
public static T New<T>(this Kubernetes client, string name) where T : IKubernetesObject<V1ObjectMeta>, new() => client.Scheme.New<T>(name);

/// <summary>Creates a new Kubernetes object of the given type and sets its <see cref="IKubernetesObject.ApiVersion"/>,
/// <see cref="IKubernetesObject.Kind"/>, <see cref="V1ObjectMeta.Namespace"/>, and <see cref="V1ObjectMeta.Name"/>.
/// </summary>
public static T New<T>(this Kubernetes client, string ns, string name) where T : IKubernetesObject<V1ObjectMeta>, new() => client.Scheme.New<T>(ns, name);

/// <summary>Creates a new <see cref="KubernetesRequest"/> using the given <see cref="HttpMethod"/>
/// (<see cref="HttpMethod.Get"/> by default).
/// </summary>
public static KubernetesRequest Request(this Kubernetes client, HttpMethod method = null) => new KubernetesRequest(client).Method(method);

/// <summary>Creates a new <see cref="KubernetesRequest"/> using the given <see cref="HttpMethod"/>
/// and resource URI components.
/// </summary>
public static KubernetesRequest Request(this Kubernetes client,
HttpMethod method, string type = null, string ns = null, string name = null, string group = null, string version = null) =>
new KubernetesRequest(client).Method(method).Group(group).Version(version).Type(type).Namespace(ns).Name(name);

/// <summary>Creates a new <see cref="KubernetesRequest"/> to access the given type of object.</summary>
public static KubernetesRequest Request(this Kubernetes client, Type type) => new KubernetesRequest(client).GVK(type);

/// <summary>Creates a new <see cref="KubernetesRequest"/> to access the given type of object with an optional name and namespace.</summary>
public static KubernetesRequest Request(this Kubernetes client, HttpMethod method, Type type, string ns = null, string name = null) =>
Request(client, method).GVK(type).Namespace(ns).Name(name);

/// <summary>Creates a new <see cref="KubernetesRequest"/> to access the given type of object with an optional name and namespace.</summary>
public static KubernetesRequest Request<T>(this Kubernetes client, string ns = null, string name = null) => Request(client, null, typeof(T), ns, name);

/// <summary>Creates a new <see cref="KubernetesRequest"/> to access the given object.</summary>
public static KubernetesRequest Request(this Kubernetes client, IKubernetesObject obj, bool setBody = true) => new KubernetesRequest(client).Set(obj, setBody);
}
}
686 changes: 686 additions & 0 deletions src/KubernetesClient/Fluent/KubernetesRequest.cs

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions src/KubernetesClient/Fluent/KubernetesResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using k8s.Models;
using Newtonsoft.Json;

namespace k8s.Fluent
{
/// <summary>Represents a response to a <see cref="KubernetesRequest"/>.</summary>
public sealed class KubernetesResponse : IDisposable
{
/// <summary>Initializes a new <see cref="KubernetesResponse"/> from an <see cref="HttpResponseMessage"/>.</summary>
public KubernetesResponse(HttpResponseMessage message) => Message = message ?? throw new ArgumentNullException(nameof(message));

/// <summary>Indicates whether the server returned an error response.</summary>
public bool IsError => (int)StatusCode >= 400;

/// <summary>Indicates whether the server returned a 404 Not Found response.</summary>
public bool IsNotFound => StatusCode == HttpStatusCode.NotFound;

/// <summary>Gets the underlying <see cref="HttpResponseMessage"/>.</summary>
public HttpResponseMessage Message { get; }

/// <summary>Gets the <see cref="HttpStatusCode"/> of the response.</summary>
public HttpStatusCode StatusCode => Message.StatusCode;

/// <inheritdoc/>
public void Dispose() => Message.Dispose();

/// <summary>Returns the response body as a string.</summary>
public async Task<string> GetBodyAsync()
{
if (body == null)
{
body = Message.Content != null ? await Message.Content.ReadAsStringAsync().ConfigureAwait(false) : string.Empty;
}
return body;
}

/// <summary>Deserializes the response body from JSON as a value of the given type, or null if the response body is empty.</summary>
/// <param name="type">The type of object to return</param>
/// <param name="failIfEmpty">If false, an empty response body will be returned as null. If true, an exception will be thrown if
/// the body is empty. The default is false.
/// </param>
public async Task<object> GetBodyAsync(Type type, bool failIfEmpty = false)
{
string body = await GetBodyAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(body))
{
if (!failIfEmpty) throw new InvalidOperationException("The response body was empty.");
return null;
}
return JsonConvert.DeserializeObject(body, type, Kubernetes.DefaultJsonSettings);
}

/// <summary>Deserializes the response body from JSON as a value of type <typeparamref name="T"/>, or the default value of
/// type <typeparamref name="T"/> if the response body is empty.
/// </summary>
/// <param name="failIfEmpty">If false, an empty response body will be returned as the default value of type
/// <typeparamref name="T"/>. If true, an exception will be thrown if the body is empty. The default is false.
/// </param>
public async Task<T> GetBodyAsync<T>(bool failIfEmpty = false)
{
string body = await GetBodyAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(body))
{
if (failIfEmpty) throw new InvalidOperationException("The response body was empty.");
return default(T);
}
return JsonConvert.DeserializeObject<T>(body, Kubernetes.DefaultJsonSettings);
}

/// <summary>Deserializes the response body as a <see cref="V1Status"/> object, or creates one from the status code if the
/// response body is not a JSON object.
/// </summary>
public async Task<V1Status> GetStatusAsync()
{
try
{
var status = await GetBodyAsync<V1Status>().ConfigureAwait(false);
if (status != null && (status.Status == "Success" || status.Status == "Failure")) return status;
}
catch (JsonException) { }
return new V1Status()
{
Status = IsError ? "Failure" : "Success", Code = (int)StatusCode, Reason = StatusCode.ToString(), Message = body
};
}

string body;
}
}
96 changes: 62 additions & 34 deletions src/KubernetesClient/Kubernetes.ConfigInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using k8s.Exceptions;
using k8s.Models;
using Microsoft.Rest;
using Newtonsoft.Json;

namespace k8s
{
Expand Down Expand Up @@ -41,9 +42,8 @@ public Kubernetes(KubernetesClientConfiguration config, HttpClient httpClient) :
public Kubernetes(KubernetesClientConfiguration config, HttpClient httpClient, bool disposeHttpClient) : this(httpClient, disposeHttpClient)
{
ValidateConfig(config);
CaCerts = config.SslCaCerts;
SkipTlsVerify = config.SkipTlsVerify;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is getting dropped?

Copy link
Contributor Author

@admilazz admilazz Apr 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config object is saved below in this.config = config, and they are accessible from there. (I'm not 100% sure what you mean by "getting dropped".)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any detail reason for this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to save the entire config object rather than just two fields out of it, because it was useful to have it when instantiating a request - in particular to construct the credentials. Now that I use the ServiceCredentials object from the Kubernetes client, this can probably be reverted. But it's not like any information is being lost here. :-)

SetCredentials(config);
this.config = config;
SetCredentials();
}

/// <summary>
Expand All @@ -59,11 +59,25 @@ public Kubernetes(KubernetesClientConfiguration config, params DelegatingHandler
: this(handlers)
{
ValidateConfig(config);
CaCerts = config.SslCaCerts;
SkipTlsVerify = config.SkipTlsVerify;
InitializeFromConfig(config);
this.config = config;
InitializeFromConfig();
}

/// <summary>Gets or sets the <see cref="KubernetesScheme"/> used to map types to their Kubernetes groups, versions, and kinds.
/// The default is <see cref="KubernetesScheme.Default"/>.
/// </summary>
/// <summary>Gets or sets the <see cref="KubernetesScheme"/> used to map types to their Kubernetes groups, version, and kinds.</summary>
public KubernetesScheme Scheme
{
get => _scheme;
set
{
if (value == null) throw new ArgumentNullException(nameof(Scheme));
_scheme = value;
}
}


private void ValidateConfig(KubernetesClientConfiguration config)
{
if (config == null)
Expand All @@ -86,7 +100,7 @@ private void ValidateConfig(KubernetesClientConfiguration config)
}
}

private void InitializeFromConfig(KubernetesClientConfiguration config)
private void InitializeFromConfig()
{
if (BaseUri.Scheme == "https")
{
Expand All @@ -107,25 +121,25 @@ private void InitializeFromConfig(KubernetesClientConfiguration config)
}
else
{
if (CaCerts == null)
if (config.SslCaCerts == null)
{
throw new KubeConfigException("A CA must be set when SkipTlsVerify === false");
}
#if NET452
((WebRequestHandler) HttpClientHandler).ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
return Kubernetes.CertificateValidationCallBack(sender, CaCerts, certificate, chain, sslPolicyErrors);
return Kubernetes.CertificateValidationCallBack(sender, config.SslCaCerts, certificate, chain, sslPolicyErrors);
};
#elif XAMARINIOS1_0
System.Net.ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) =>
{
var cert = new X509Certificate2(certificate);
return Kubernetes.CertificateValidationCallBack(sender, CaCerts, cert, chain, sslPolicyErrors);
return Kubernetes.CertificateValidationCallBack(sender, config.SslCaCerts, cert, chain, sslPolicyErrors);
};
#elif MONOANDROID8_1
var certList = new System.Collections.Generic.List<Java.Security.Cert.Certificate>();

foreach (X509Certificate2 caCert in CaCerts)
foreach (X509Certificate2 caCert in config.SslCaCerts)
{
using (var certStream = new System.IO.MemoryStream(caCert.RawData))
{
Expand All @@ -141,21 +155,17 @@ private void InitializeFromConfig(KubernetesClientConfiguration config)
#else
HttpClientHandler.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
return Kubernetes.CertificateValidationCallBack(sender, CaCerts, certificate, chain, sslPolicyErrors);
return Kubernetes.CertificateValidationCallBack(sender, config.SslCaCerts, certificate, chain, sslPolicyErrors);
};
#endif
}
}

// set credentails for the kubernetes client
SetCredentials(config);
// set credentials for the kubernetes client
SetCredentials();
config.AddCertificates(HttpClientHandler);
}

private X509Certificate2Collection CaCerts { get; }

private bool SkipTlsVerify { get; }

partial void CustomInitialize()
{
#if NET452
Expand Down Expand Up @@ -189,26 +199,16 @@ partial void CustomInitialize()
}

/// <summary>
/// Set credentials for the Client
/// Set credentials for the Client based on the config
/// </summary>
/// <param name="config">k8s client configuration</param>
private void SetCredentials(KubernetesClientConfiguration config)
private void SetCredentials()
{
// set the Credentails for token based auth
if (!string.IsNullOrWhiteSpace(config.AccessToken))
{
Credentials = new TokenCredentials(config.AccessToken);
}
else if (!string.IsNullOrWhiteSpace(config.Username) && !string.IsNullOrWhiteSpace(config.Password))
{
Credentials = new BasicAuthenticationCredentials
{
UserName = config.Username,
Password = config.Password
};
}
Credentials = CreateCredentials(config);
}

internal readonly KubernetesClientConfiguration config;
private KubernetesScheme _scheme = KubernetesScheme.Default;

/// <summary>
/// SSl Cert Validation Callback
/// </summary>
Expand Down Expand Up @@ -264,5 +264,33 @@ public static bool CertificateValidationCallBack(
// In all other cases, return false.
return false;
}

/// <summary>Creates the JSON serializer settings used for serializing request bodies and deserializing responses.</summary>
public static JsonSerializerSettings CreateSerializerSettings()
{
var settings = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore };
settings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
return settings;
}

/// <summary>Creates <see cref="ServiceClientCredentials"/> from a Kubernetes configuration, or returns null if the configuration
/// contains no credentials of that type.
/// </summary>
internal static ServiceClientCredentials CreateCredentials(KubernetesClientConfiguration config)
{
if (config == null) throw new ArgumentNullException(nameof(config));
if (!string.IsNullOrEmpty(config.AccessToken))
{
return new TokenCredentials(config.AccessToken);
}
else if (!string.IsNullOrEmpty(config.Username))
{
return new BasicAuthenticationCredentials() { UserName = config.Username, Password = config.Password };
}
return null;
}

/// <summary>Gets the <see cref="JsonSerializerSettings"/> used to serialize and deserialize Kubernetes objects.</summary>
internal static readonly JsonSerializerSettings DefaultJsonSettings = CreateSerializerSettings();
}
}
12 changes: 6 additions & 6 deletions src/KubernetesClient/Kubernetes.WebSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,19 +277,19 @@ public partial class Kubernetes
}

#if (NET452 || NETSTANDARD2_0)
if (this.CaCerts != null)
if (this.config?.SslCaCerts != null)
{
webSocketBuilder.SetServerCertificateValidationCallback(this.ServerCertificateValidationCallback);
}
#endif

#if NETCOREAPP2_1
if (this.CaCerts != null)
if (this.config?.SslCaCerts != null)
{
webSocketBuilder.ExpectServerCertificate(this.CaCerts);
webSocketBuilder.ExpectServerCertificate(this.config.SslCaCerts);
}

if (this.SkipTlsVerify)
if (this.config?.SkipTlsVerify == true)
{
webSocketBuilder.SkipServerCertificateValidation();
}
Expand Down Expand Up @@ -365,7 +365,7 @@ public partial class Kubernetes
}

#if (NET452 || NETSTANDARD2_0)
if (this.CaCerts != null)
if (this.config?.SslCaCerts != null)
{
webSocketBuilder.CleanupServerCertificateValidationCallback(this.ServerCertificateValidationCallback);
}
Expand All @@ -377,7 +377,7 @@ public partial class Kubernetes
#if (NET452 || NETSTANDARD2_0)
internal bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
return Kubernetes.CertificateValidationCallBack(sender, this.CaCerts, certificate, chain, sslPolicyErrors);
return Kubernetes.CertificateValidationCallBack(sender, this.config?.SslCaCerts, certificate, chain, sslPolicyErrors);
}
#endif
}
Expand Down
Loading