Skip to content

Add a fluent Kubernetes API #406

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 13 commits into from
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