Skip to content

Commit d90289a

Browse files
qmfrederikbrendandburns
authored andcommitted
Support relative paths in Kubernetes configuration files (#141)
* Support relative paths in Kubernetes configuration files * Filename -> FileName * Filename -> FileName * KuberentesClientConfiguration: Allow the user to opt-out of the mechanism which resolves relative paths in the configuration file. * Update unit tests * Fix test
1 parent eea4c88 commit d90289a

File tree

5 files changed

+182
-40
lines changed

5 files changed

+182
-40
lines changed

src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ public class K8SConfiguration
5252
/// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields.
5353
/// </summary>
5454
[YamlMember(Alias = "extensions")]
55-
public IDictionary<string, dynamic> Extensions { get; set; }
55+
public IDictionary<string, dynamic> Extensions { get; set; }
56+
57+
/// <summary>
58+
/// Gets or sets the name of the Kubernetes configuration file. This property is set only when the configuration
59+
/// was loaded from disk, and can be used to resolve relative paths.
60+
/// </summary>
61+
[YamlIgnore]
62+
public string FileName { get; set; }
5663
}
5764
}

src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,32 @@ public partial class KubernetesClientConfiguration
2828
/// Initializes a new instance of the <see cref="KubernetesClientConfiguration" /> from config file
2929
/// </summary>
3030
/// <param name="masterUrl">kube api server endpoint</param>
31-
/// <param name="kubeconfigPath">Explicit file path to kubeconfig. Set to null to use the default file path</param>
31+
/// <param name="kubeconfigPath">Explicit file path to kubeconfig. Set to null to use the default file path</param>
32+
/// <param name="useRelativePaths">When <see langword="true"/>, the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
33+
/// file is located. When <see langword="false"/>, the paths will be considered to be relative to the current working directory.</param>
3234
public static KubernetesClientConfiguration BuildConfigFromConfigFile(string kubeconfigPath = null,
33-
string currentContext = null, string masterUrl = null)
35+
string currentContext = null, string masterUrl = null, bool useRelativePaths = true)
3436
{
3537
return BuildConfigFromConfigFile(new FileInfo(kubeconfigPath ?? KubeConfigDefaultLocation), null,
36-
masterUrl);
38+
masterUrl, useRelativePaths);
3739
}
3840

3941
/// <summary>
4042
/// </summary>
4143
/// <param name="kubeconfig">Fileinfo of the kubeconfig, cannot be null</param>
4244
/// <param name="currentContext">override the context in config file, set null if do not want to override</param>
43-
/// <param name="masterUrl">overrider kube api server endpoint, set null if do not want to override</param>
45+
/// <param name="masterUrl">override the kube api server endpoint, set null if do not want to override</param>
46+
/// <param name="useRelativePaths">When <see langword="true"/>, the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
47+
/// file is located. When <see langword="false"/>, the paths will be considered to be relative to the current working directory.</param>
4448
public static KubernetesClientConfiguration BuildConfigFromConfigFile(FileInfo kubeconfig,
45-
string currentContext = null, string masterUrl = null)
49+
string currentContext = null, string masterUrl = null, bool useRelativePaths = true)
4650
{
4751
if (kubeconfig == null)
4852
{
4953
throw new NullReferenceException(nameof(kubeconfig));
5054
}
5155

52-
var k8SConfig = LoadKubeConfig(kubeconfig);
56+
var k8SConfig = LoadKubeConfig(kubeconfig, useRelativePaths);
5357
var k8SConfiguration = GetKubernetesClientConfiguration(currentContext, masterUrl, k8SConfig);
5458

5559
return k8SConfiguration;
@@ -172,7 +176,7 @@ private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext
172176
}
173177
else if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthority))
174178
{
175-
SslCaCert = new X509Certificate2(clusterDetails.ClusterEndpoint.CertificateAuthority);
179+
SslCaCert = new X509Certificate2(GetFullPath(k8SConfig, clusterDetails.ClusterEndpoint.CertificateAuthority));
176180
}
177181
}
178182
}
@@ -230,8 +234,8 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
230234
if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientCertificate) &&
231235
!string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientKey))
232236
{
233-
ClientCertificateFilePath = userDetails.UserCredentials.ClientCertificate;
234-
ClientKeyFilePath = userDetails.UserCredentials.ClientKey;
237+
ClientCertificateFilePath = GetFullPath(k8SConfig, userDetails.UserCredentials.ClientCertificate);
238+
ClientKeyFilePath = GetFullPath(k8SConfig, userDetails.UserCredentials.ClientKey);
235239
userCredentialsFound = true;
236240
}
237241

@@ -245,31 +249,37 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
245249
/// <summary>
246250
/// Loads entire Kube Config from default or explicit file path
247251
/// </summary>
248-
/// <param name="kubeconfigPath">Explicit file path to kubeconfig. Set to null to use the default file path</param>
249-
/// <returns></returns>
250-
public static async Task<K8SConfiguration> LoadKubeConfigAsync(string kubeconfigPath = null)
252+
/// <param name="kubeconfigPath">Explicit file path to kubeconfig. Set to null to use the default file path</param>
253+
/// <param name="useRelativePaths">When <see langword="true"/>, the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
254+
/// file is located. When <see langword="false"/>, the paths will be considered to be relative to the current working directory.</param>
255+
/// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns>
256+
public static async Task<K8SConfiguration> LoadKubeConfigAsync(string kubeconfigPath = null, bool useRelativePaths = true)
251257
{
252258
var fileInfo = new FileInfo(kubeconfigPath ?? KubeConfigDefaultLocation);
253259

254-
return await LoadKubeConfigAsync(fileInfo);
260+
return await LoadKubeConfigAsync(fileInfo, useRelativePaths);
255261
}
256262

257263
/// <summary>
258264
/// Loads entire Kube Config from default or explicit file path
259265
/// </summary>
260-
/// <param name="kubeconfigPath">Explicit file path to kubeconfig. Set to null to use the default file path</param>
261-
/// <returns></returns>
262-
public static K8SConfiguration LoadKubeConfig(string kubeconfigPath = null)
266+
/// <param name="kubeconfigPath">Explicit file path to kubeconfig. Set to null to use the default file path</param>
267+
/// <param name="useRelativePaths">When <see langword="true"/>, the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
268+
/// file is located. When <see langword="false"/>, the paths will be considered to be relative to the current working directory.</param>
269+
/// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns>
270+
public static K8SConfiguration LoadKubeConfig(string kubeconfigPath = null, bool useRelativePaths = true)
263271
{
264-
return LoadKubeConfigAsync(kubeconfigPath).GetAwaiter().GetResult();
272+
return LoadKubeConfigAsync(kubeconfigPath, useRelativePaths).GetAwaiter().GetResult();
265273
}
266-
274+
267275
// <summary>
268276
/// Loads Kube Config
269277
/// </summary>
270-
/// <param name="kubeconfig">Kube config file contents</param>
278+
/// <param name="kubeconfig">Kube config file contents</param>
279+
/// <param name="useRelativePaths">When <see langword="true"/>, the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
280+
/// file is located. When <see langword="false"/>, the paths will be considered to be relative to the current working directory.</param>
271281
/// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns>
272-
public static async Task<K8SConfiguration> LoadKubeConfigAsync(FileInfo kubeconfig)
282+
public static async Task<K8SConfiguration> LoadKubeConfigAsync(FileInfo kubeconfig, bool useRelativePaths = true)
273283
{
274284
if (!kubeconfig.Exists)
275285
{
@@ -278,18 +288,27 @@ public static async Task<K8SConfiguration> LoadKubeConfigAsync(FileInfo kubeconf
278288

279289
using (var stream = kubeconfig.OpenRead())
280290
{
281-
return await Yaml.LoadFromStreamAsync<K8SConfiguration>(stream);
291+
var config = await Yaml.LoadFromStreamAsync<K8SConfiguration>(stream);
292+
293+
if (useRelativePaths)
294+
{
295+
config.FileName = kubeconfig.FullName;
296+
}
297+
298+
return config;
282299
}
283300
}
284301

285302
/// <summary>
286303
/// Loads Kube Config
287304
/// </summary>
288-
/// <param name="kubeconfig">Kube config file contents</param>
305+
/// <param name="kubeconfig">Kube config file contents</param>
306+
/// <param name="useRelativePaths">When <see langword="true"/>, the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
307+
/// file is located. When <see langword="false"/>, the paths will be considered to be relative to the current working directory.</param>
289308
/// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns>
290-
public static K8SConfiguration LoadKubeConfig(FileInfo kubeconfig)
309+
public static K8SConfiguration LoadKubeConfig(FileInfo kubeconfig, bool useRelativePaths = true)
291310
{
292-
return LoadKubeConfigAsync(kubeconfig).GetAwaiter().GetResult();
311+
return LoadKubeConfigAsync(kubeconfig, useRelativePaths).GetAwaiter().GetResult();
293312
}
294313

295314
// <summary>
@@ -311,5 +330,35 @@ public static K8SConfiguration LoadKubeConfig(Stream kubeconfigStream)
311330
{
312331
return LoadKubeConfigAsync(kubeconfigStream).GetAwaiter().GetResult();
313332
}
333+
334+
/// <summary>
335+
/// Tries to get the full path to a file referenced from the Kubernetes configuration.
336+
/// </summary>
337+
/// <param name="configuration">
338+
/// The Kubernetes configuration.
339+
/// </param>
340+
/// <param name="path">
341+
/// The path to resolve.
342+
/// </param>
343+
/// <returns>
344+
/// When possible a fully qualified path to the file.
345+
/// </returns>
346+
/// <remarks>
347+
/// For example, if the configuration file is at "C:\Users\me\kube.config" and path is "ca.crt",
348+
/// this will return "C:\Users\me\ca.crt". Similarly, if path is "D:\ca.cart", this will return
349+
/// "D:\ca.crt".
350+
/// </remarks>
351+
private static string GetFullPath(K8SConfiguration configuration, string path)
352+
{
353+
// If we don't have a file name,
354+
if (string.IsNullOrWhiteSpace(configuration.FileName) || Path.IsPathRooted(path))
355+
{
356+
return path;
357+
}
358+
else
359+
{
360+
return Path.Combine(Path.GetDirectoryName(configuration.FileName), path);
361+
}
362+
}
314363
}
315364
}

tests/KubernetesClient.Tests/CertUtilsTests.cs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,37 @@ namespace k8s.Tests
88
public class CertUtilsTests
99
{
1010
/// <summary>
11-
/// This file contains a sample kubeconfig file
11+
/// This file contains a sample kubeconfig file. The paths to the certificate files are relative
12+
/// to the current working directly.
1213
/// </summary>
13-
private static readonly string kubeConfigFileName = "assets/kubeconfig.yml";
14+
private static readonly string kubeConfigFileName = "assets/kubeconfig.yml";
15+
16+
/// <summary>
17+
/// This file contains a sample kubeconfig file. The paths to the certificate files are relative
18+
/// to the directory in which the kubeconfig file is located.
19+
/// </summary>
20+
private static readonly string kubeConfigWithRelativePathsFileName = "assets/kubeconfig.relative.yml";
1421

1522
/// <summary>
1623
/// Checks that a certificate can be loaded from files.
1724
/// </summary>
1825
[Fact]
1926
public void LoadFromFiles()
2027
{
21-
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(kubeConfigFileName, "federal-context");
28+
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(kubeConfigFileName, "federal-context", useRelativePaths: false);
29+
30+
// Just validate that this doesn't throw and private key is non-null
31+
var cert = CertUtils.GeneratePfx(cfg);
32+
Assert.NotNull(cert.PrivateKey);
33+
}
34+
35+
/// <summary>
36+
/// Checks that a certificate can be loaded from files, in a scenario where the files are using relative paths.
37+
/// </summary>
38+
[Fact]
39+
public void LoadFromFilesRelativePath()
40+
{
41+
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(kubeConfigWithRelativePathsFileName, "federal-context");
2242

2343
// Just validate that this doesn't throw and private key is non-null
2444
var cert = CertUtils.GeneratePfx(cfg);
@@ -31,7 +51,20 @@ public void LoadFromFiles()
3151
[Fact]
3252
public void LoadFromInlineData()
3353
{
34-
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(kubeConfigFileName, "victorian-context");
54+
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(kubeConfigFileName, "victorian-context", useRelativePaths: false);
55+
56+
// Just validate that this doesn't throw and private key is non-null
57+
var cert = CertUtils.GeneratePfx(cfg);
58+
Assert.NotNull(cert.PrivateKey);
59+
}
60+
61+
/// <summary>
62+
/// Checks that a certificate can be loaded from inline, in a scenario where the files are using relative paths..
63+
/// </summary>
64+
[Fact]
65+
public void LoadFromInlineDataRelativePath()
66+
{
67+
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(kubeConfigWithRelativePathsFileName, "victorian-context");
3568

3669
// Just validate that this doesn't throw and private key is non-null
3770
var cert = CertUtils.GeneratePfx(cfg);

tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class KubernetesClientConfigurationTests
1717
public void ContextHost(string context, string host)
1818
{
1919
var fi = new FileInfo("assets/kubeconfig.yml");
20-
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, context);
20+
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, context, useRelativePaths: false);
2121
Assert.Equal(host, cfg.Host);
2222
}
2323

@@ -48,7 +48,7 @@ public void ContextUserToken(string context, string token)
4848
public void ContextCertificate(string context, string clientCert, string clientCertKey)
4949
{
5050
var fi = new FileInfo("assets/kubeconfig.yml");
51-
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, context);
51+
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, context, useRelativePaths: false);
5252
Assert.Equal(context, cfg.CurrentContext);
5353
Assert.Equal(cfg.ClientCertificateFilePath, clientCert);
5454
Assert.Equal(cfg.ClientKeyFilePath, clientCertKey);
@@ -144,7 +144,7 @@ public void ContextNotFound()
144144
[Fact]
145145
public void DefaultConfigurationLoaded()
146146
{
147-
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(new FileInfo("assets/kubeconfig.yml"));
147+
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(new FileInfo("assets/kubeconfig.yml"), useRelativePaths: false);
148148
Assert.NotNull(cfg.Host);
149149
}
150150

@@ -155,7 +155,7 @@ public void DefaultConfigurationLoaded()
155155
public void IncompleteUserCredentials()
156156
{
157157
var fi = new FileInfo("assets/kubeconfig.no-credentials.yml");
158-
Assert.Throws<KubeConfigException>(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi));
158+
Assert.Throws<KubeConfigException>(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, useRelativePaths: false));
159159
}
160160

161161
/// <summary>
@@ -196,7 +196,7 @@ public void ServerNotFound()
196196
public void UserPasswordAuthentication()
197197
{
198198
var fi = new FileInfo("assets/kubeconfig.user-pass.yml");
199-
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi);
199+
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, useRelativePaths: false);
200200
Assert.Equal("admin", cfg.Username);
201201
Assert.Equal("secret", cfg.Password);
202202
}
@@ -208,7 +208,7 @@ public void UserPasswordAuthentication()
208208
public void UserNotFound()
209209
{
210210
var fi = new FileInfo("assets/kubeconfig.user-not-found.yml");
211-
Assert.Throws<KubeConfigException>(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi));
211+
Assert.Throws<KubeConfigException>(() => KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, useRelativePaths: false));
212212
}
213213

214214
/// <summary>
@@ -218,7 +218,7 @@ public void UserNotFound()
218218
public void EmptyUserNotFound()
219219
{
220220
var fi = new FileInfo("assets/kubeconfig.no-user.yml");
221-
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi);
221+
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, useRelativePaths: false);
222222

223223
Assert.NotEmpty(cfg.Host);
224224
}
@@ -230,7 +230,7 @@ public void EmptyUserNotFound()
230230
public void OverrideByMasterUrl()
231231
{
232232
var fi = new FileInfo("assets/kubeconfig.yml");
233-
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, masterUrl: "http://test.server");
233+
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, masterUrl: "http://test.server", useRelativePaths: false);
234234
Assert.Equal("http://test.server", cfg.Host);
235235
}
236236

@@ -280,7 +280,7 @@ public void DeletedConfigurationFile()
280280

281281
try
282282
{
283-
config = KubernetesClientConfiguration.BuildConfigFromConfigFile(tempFileInfo);
283+
config = KubernetesClientConfiguration.BuildConfigFromConfigFile(tempFileInfo, useRelativePaths: false);
284284
}
285285
finally
286286
{
@@ -295,7 +295,7 @@ public void DeletedConfigurationFile()
295295
public void DefaultConfigurationAsStringLoaded()
296296
{
297297
var filePath = "assets/kubeconfig.yml";
298-
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(filePath, null, null);
298+
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(filePath, null, null, useRelativePaths: false);
299299
Assert.NotNull(cfg.Host);
300300
}
301301

@@ -321,7 +321,7 @@ public void AsUserExtra()
321321
{
322322
var filePath = "assets/kubeconfig.as-user-extra.yml";
323323

324-
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(filePath, null, null);
324+
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(filePath, null, null, useRelativePaths: false);
325325
Assert.NotNull(cfg.Host);
326326
}
327327

0 commit comments

Comments
 (0)