Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/KubeClient.Extensions.WebSockets/K8sWebSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

namespace KubeClient.Extensions.WebSockets
{
using KubeClient.Utilities;

/// <summary>
/// Connection factory for Kubernetes web sockets.
/// </summary>
Expand Down Expand Up @@ -173,7 +175,7 @@ private static byte[] BuildRequestHeader(Uri uri, K8sWebSocketOptions options, s
{
StringBuilder builder = new StringBuilder()
.Append("GET ")
.Append(uri.PathAndQuery)
.Append(uri.SafeGetPathAndQuery())
.Append(" HTTP/1.1\r\n");

// Add all of the required headers, honoring Host header if set.
Expand Down
10 changes: 6 additions & 4 deletions src/KubeClient/ResourceClients/KubeResourceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@

namespace KubeClient.ResourceClients
{
using KubeClient.Models.ContractResolvers;
using Models;
using Models.ContractResolvers;
using Models.Converters;
using Utilities;

/// <summary>
/// The base class for Kubernetes resource API clients.
Expand Down Expand Up @@ -593,9 +594,10 @@ protected IObservable<string> ObserveLines(Func<HttpRequest> requestFactory, str

try
{
HttpRequest request = requestFactory();

logger.LogDebug("Start streaming {RequestMethod} request for {RequestUri}...", HttpMethod.Get.Method, request.Uri.PathAndQuery);
HttpRequest request = requestFactory().WithRequestAction((HttpRequestMessage requestMessage) =>
{
logger.LogDebug("Start streaming {RequestMethod} request for {RequestUri}...", HttpMethod.Get, requestMessage.RequestUri.SafeGetPathAndQuery());
});

using (HttpResponseMessage responseMessage = await Http.GetStreamedAsync(request, subscriptionCancellationToken).ConfigureAwait(false))
{
Expand Down
59 changes: 59 additions & 0 deletions src/KubeClient/Utilities/HttpRequestHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using HTTPlease;
using HTTPlease.Core;
using System;
using System.Net.Http;

namespace KubeClient.Utilities
{
/// <summary>
/// Helper methods for working with <see cref="HttpRequest"/> and related types.
/// </summary>
public static class HttpRequestHelper
{
/// <summary>
/// Expand an <see cref="HttpRequest"/>'s request URI, populating its URI template if necessary.
/// </summary>
/// <param name="request">
/// The target <see cref="HttpRequest"/>.
/// </param>
/// <param name="baseUri">
/// The base URI to use (optional, unless <see cref="HttpRequestBase.Uri"/> is <c>null</c> or not an absolute URI).
/// </param>
/// <returns>
/// The expanded request URI (always an absolute URI).
/// </returns>
public static Uri ExpandRequestUri(this HttpRequest request, Uri baseUri = null)
{
using (HttpRequestMessage requestMessage = request.BuildRequestMessage(HttpMethod.Get, body: null, baseUri))
{
return requestMessage.RequestUri;
}
}

/// <summary>
/// Expand an <see cref="HttpRequest{TContext}"/>'s request URI, populating its URI template if necessary.
/// </summary>
/// <typeparam name="TContext">
/// The type of object used as a context when evaluating request template parameters.
/// </typeparam>
/// <param name="request">
/// The target <see cref="HttpRequest{TContext}"/>.
/// </param>
/// <param name="context">
/// The <typeparamref name="TContext"/> used as a context when evaluating request template parameters.
/// </param>
/// <param name="baseUri">
/// The base URI to use (optional, unless <see cref="HttpRequestBase.Uri"/> is <c>null</c> or not an absolute URI).
/// </param>
/// <returns>
/// The expanded request URI (always an absolute URI).
/// </returns>
public static Uri ExpandRequestUri<TContext>(this HttpRequest<TContext> request, TContext context, Uri baseUri = null)
{
using (HttpRequestMessage requestMessage = request.BuildRequestMessage(HttpMethod.Get, context, body: null, baseUri))
{
return requestMessage.RequestUri;
}
}
}
}
58 changes: 58 additions & 0 deletions src/KubeClient/Utilities/UriHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;

namespace KubeClient.Utilities
{
/// <summary>
/// Helper methods for working with <see cref="Uri"/>s.
/// </summary>
public static class UriHelper
{
/// <summary>
/// A dummy URI to be used as the base URI when dealing with relative URIs.
/// </summary>
static readonly Uri DummyBaseUri = new Uri("https://dummy-host");

/// <summary>
/// Get the path (and, if present, the query) of a URI.
/// </summary>
/// <param name="uri">
/// The target <see cref="Uri"/>.
/// </param>
/// <returns>
/// The URI's path and query.
/// </returns>
/// <remarks>
/// Unlike <see cref="Uri.PathAndQuery"/>, also handles relative URIs.
/// </remarks>
public static string SafeGetPathAndQuery(this Uri uri)
{
if (uri == null)
throw new ArgumentNullException(nameof(uri));

return uri.EnsureAbsolute().PathAndQuery;
}

/// <summary>
/// Ensure that the URI is an absolute URI.
/// </summary>
/// <param name="uri">
/// The target URI.
/// </param>
/// <returns>
/// The target URI, if <see cref="Uri.IsAbsoluteUri"/> is <c>true</c>; otherwise, an absolute URI using <see cref="DummyBaseUri"/> as the base URI.
/// </returns>
static Uri EnsureAbsolute(this Uri uri)
{
if (uri == null)
throw new ArgumentNullException(nameof(uri));

if (uri.IsAbsoluteUri)
return uri;

// Slightly ugly, but System.Uri doesn't attempt to parse relative URIs so we have to convert it to an absolute URI.
Uri absoluteUri = new Uri(DummyBaseUri, relativeUri: uri);

return absoluteUri;
}
}
}
3 changes: 2 additions & 1 deletion test/KubeClient.Tests/Logging/TestLogger.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Net.Http;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -104,5 +105,5 @@ public void Log<TState>(LogLevel level, EventId eventId, TState state, Exception
/// An <see cref="IDisposable"/> that ends the logical operation scope when disposed.
/// </returns>
public IDisposable BeginScope<TState>(TState state) => Disposable.Empty;
}
}
}
56 changes: 53 additions & 3 deletions test/KubeClient.Tests/LoggingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
using HTTPlease.Diagnostics;
using HTTPlease.Formatters;
using HTTPlease.Testability;
using HTTPlease.Testability.Mocks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Xunit;
using Newtonsoft.Json;

namespace KubeClient.Tests
{
using Logging;
using Models;
using Utilities;

/// <summary>
/// Tests for <see cref="KubeApiClient"/> logging.
Expand Down Expand Up @@ -50,6 +50,7 @@ public async Task PodsV1_GetByName_NotFound()
return request.CreateResponse(HttpStatusCode.NotFound,
responseBody: JsonConvert.SerializeObject(new StatusV1
{
Status = "Failure",
Reason = "NotFound"
}),
WellKnownMediaTypes.Json
Expand Down Expand Up @@ -165,5 +166,54 @@ public async Task PodsV1_GetByName_OK()
logEntry2.Properties["StatusCode"]
);
}

/// <summary>
/// Verify that the client's logger emits the correct log entry from a custom request action.
/// </summary>
[Fact(DisplayName = "Emit log entry from custom request action")]
public async Task Custom_Request_Action()
{
var logEntries = new List<LogEntry>();

TestLogger logger = new TestLogger(LogLevel.Information);
logger.LogEntries.Subscribe(
logEntry => logEntries.Add(logEntry)
);

ClientBuilder clientBuilder = new ClientBuilder()
.WithLogging(logger);

HttpClient httpClient = clientBuilder.CreateClient("http://localhost:1234/api", TestHandlers.RespondWith(request =>
{
return request.CreateResponse(HttpStatusCode.OK,
responseBody: JsonConvert.SerializeObject(new StatusV1
{
Status = "Success",
}),
WellKnownMediaTypes.Json
);
}));

HttpRequest request = HttpRequest.Create("{Foo}/{Bar}?Baz={Baz}")
.WithRequestAction((HttpRequestMessage requestMessage) =>
{
logger.LogDebug("Start streaming {RequestMethod} request for {RequestUri}...", HttpMethod.Get, requestMessage.RequestUri.SafeGetPathAndQuery());
}).WithTemplateParameters(new
{
Foo = "foo",
Bar = "bAr",
Baz = "b4z",
});

using (HttpResponseMessage response = await httpClient.GetAsync(request))
{
// [0] = Custom message, [1] = Begin request, [2] End request
Assert.Equal(3, logEntries.Count);

Assert.Equal("Start streaming GET request for /api/foo/bAr?Baz=b4z...", logEntries[0].Message);

response.EnsureSuccessStatusCode();
}
}
}
}
96 changes: 96 additions & 0 deletions test/KubeClient.Tests/UriHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace KubeClient.Tests
{
using Utilities;
using Xunit;

/// <summary>
/// Tests for <see cref="UriHelper"/>.
/// </summary>
public class UriHelperTests
{
static readonly Uri BaseUri = new Uri("https://localhost");

[Theory]
[InlineData("/")]
[InlineData("/?param1=value1&param2=value2")]
[InlineData("path1")]
[InlineData("path1?param1=value1&param2=value2")]
[InlineData("path1/path2")]
[InlineData("path1/path2?param1=value1&param2=value2")]
[InlineData("path1/path2/")]
[InlineData("path1/path2/?param1=value1&param2=value2")]
[InlineData("/path1")]
[InlineData("/path1?param1=value1&param2=value2")]
[InlineData("/path1/path2")]
[InlineData("/path1/path2?param1=value1&param2=value2")]
[InlineData("/path1/path2/")]
[InlineData("/path1/path2/?param1=value1&param2=value2")]
public void Can_SafeGetPathAndQuery_RelativeUri(string input)
{
Uri uri = new Uri(input, UriKind.RelativeOrAbsolute);
Assert.False(uri.IsAbsoluteUri);

string pathAndQuery = uri.SafeGetPathAndQuery();
Assert.Equal(pathAndQuery,
NormalizePath(input)
);
}

[Theory]
[InlineData("/")]
[InlineData("/?param1=value1&param2=value2")]
[InlineData("path1")]
[InlineData("path1?param1=value1&param2=value2")]
[InlineData("path1/path2")]
[InlineData("path1/path2?param1=value1&param2=value2")]
[InlineData("path1/path2/")]
[InlineData("path1/path2/?param1=value1&param2=value2")]
[InlineData("/path1")]
[InlineData("/path1?param1=value1&param2=value2")]
[InlineData("/path1/path2")]
[InlineData("/path1/path2?param1=value1&param2=value2")]
[InlineData("/path1/path2/")]
[InlineData("/path1/path2/?param1=value1&param2=value2")]
public void Can_SafeGetPathAndQuery_AbsoluteUri(string input)
{
Uri relativeUri = new Uri(input, UriKind.RelativeOrAbsolute);
Assert.False(relativeUri.IsAbsoluteUri);

Uri uri = new Uri(BaseUri, relativeUri);

string pathAndQuery = uri.SafeGetPathAndQuery();
Assert.Equal(pathAndQuery,
NormalizePath(input)
);
}

/// <summary>
/// Normalise the specified path and query for comparisons in tests.
/// </summary>
/// <param name="pathAndQuery">
/// The URI path and query components.
/// </param>
/// <returns>
/// The normalised path and query.
/// </returns>
/// <remarks>
/// System.Uri treats the path component of an absolute URI as an absolute path.
/// </remarks>
static string NormalizePath(string pathAndQuery)
{
if (pathAndQuery == null)
throw new ArgumentNullException(nameof(pathAndQuery));

if (pathAndQuery.StartsWith("/"))
return pathAndQuery;

return $"/{pathAndQuery}";
}
}
}