Skip to content

Commit 2efc384

Browse files
committed
Handle empty response for secrets from Kubernetes
1 parent c833bfe commit 2efc384

File tree

4 files changed

+139
-14
lines changed

4 files changed

+139
-14
lines changed

Dockerfile

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
1-
FROM mcr.microsoft.com/dotnet/core/sdk:3.0 AS installer-env
1+
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS installer-env
22

33
ENV PublishWithAspNetCoreTargetManifest false
44

55
COPY . /workingdir
66

77
RUN cd workingdir && \
8-
dotnet build WebJobs.Script.sln && \
98
dotnet publish src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj --output /azure-functions-host
109

1110
# Runtime image
12-
FROM mcr.microsoft.com/azure-functions/python:2.0
11+
FROM mcr.microsoft.com/azure-functions/python:3.0
1312

1413
RUN apt-get update && \
1514
apt-get install -y gnupg && \
16-
curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
15+
curl -sL https://deb.nodesource.com/setup_12.x | bash - && \
1716
apt-get update && \
18-
apt-get install -y nodejs dotnet-sdk-3.0
17+
apt-get install -y nodejs dotnet-sdk-3.1
18+
19+
# Install the dependencies for Visual Studio Remote Debugger
20+
RUN apt-get update && apt-get install -y --no-install-recommends unzip procps
21+
22+
# Install Visual Studio Remote Debugger
23+
RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/vsdbg
1924

2025
COPY --from=installer-env ["/azure-functions-host", "/azure-functions-host"]
2126

src/WebJobs.Script.WebHost/Security/KeyManagement/DefaultSecretManagerProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ internal ISecretsRepository CreateSecretsRepository()
7272
}
7373
else if (secretStorageType != null && secretStorageType.Equals("kubernetes", StringComparison.OrdinalIgnoreCase))
7474
{
75-
return new KubernetesSecretsRepository(_environment, new SimpleKubernetesClient(_environment));
75+
return new KubernetesSecretsRepository(_environment, new SimpleKubernetesClient(_environment, _loggerFactory.CreateLogger<SimpleKubernetesClient>()));
7676
}
7777
else if (secretStorageSas != null)
7878
{

src/WebJobs.Script.WebHost/Security/KeyManagement/SimpleKubernetesClient.cs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using System.Threading;
1414
using System.Threading.Tasks;
1515
using Microsoft.Azure.WebJobs.Script.IO;
16+
using Microsoft.Extensions.Logging;
1617
using Microsoft.Net.Http.Headers;
1718
using Newtonsoft.Json;
1819
using Newtonsoft.Json.Linq;
@@ -27,18 +28,20 @@ public class SimpleKubernetesClient : IKubernetesClient, IDisposable
2728
private const string KubernetesSecretsDir = "/run/secrets/functions-keys";
2829
private readonly HttpClient _httpClient;
2930
private readonly IEnvironment _environment;
31+
private readonly ILogger _logger;
3032
private Action _watchCallback;
3133
private bool _disposed;
3234
private AutoRecoveringFileSystemWatcher _fileWatcher;
3335

34-
public SimpleKubernetesClient(IEnvironment environment) : this(environment, CreateHttpClient())
36+
public SimpleKubernetesClient(IEnvironment environment, ILogger logger) : this(environment, CreateHttpClient(), logger)
3537
{ }
3638

3739
// for testing
38-
internal SimpleKubernetesClient(IEnvironment environment, HttpClient client)
40+
internal SimpleKubernetesClient(IEnvironment environment, HttpClient client, ILogger logger)
3941
{
4042
_httpClient = client;
4143
_environment = environment;
44+
_logger = logger;
4245
Task.Run(() => RunWatcher());
4346
}
4447

@@ -58,7 +61,9 @@ public async Task<IDictionary<string, string>> GetSecrets()
5861
}
5962
else
6063
{
61-
throw new InvalidOperationException($"{nameof(KubernetesSecretsRepository)} requires setting {EnvironmentSettingNames.AzureWebJobsKubernetesSecretName} or mounting secrets to {KubernetesSecretsDir}");
64+
var exception = new InvalidOperationException($"{nameof(KubernetesSecretsRepository)} requires setting {EnvironmentSettingNames.AzureWebJobsKubernetesSecretName} or mounting secrets to {KubernetesSecretsDir}");
65+
_logger.LogError(exception, "Error geting secrets");
66+
throw exception;
6267
}
6368
}
6469

@@ -74,6 +79,12 @@ public async Task UpdateSecrets(IDictionary<string, string> data)
7479
using (var request = await GetRequest(HttpMethod.Patch, url, new[] { new { op = "replace", path = "/data", value = data } }, "application/json-patch+json"))
7580
{
7681
var response = await _httpClient.SendAsync(request);
82+
if (!response.IsSuccessStatusCode)
83+
{
84+
_logger.LogError("Error updating Kubernetes secrets. {StatusCode}: {Content}",
85+
response.StatusCode,
86+
await response.Content.ReadAsStringAsync());
87+
}
7788
response.EnsureSuccessStatusCode();
7889
}
7990
}
@@ -84,6 +95,15 @@ public void OnSecretChange(Action callback)
8495
}
8596

8697
private async Task RunWatcher()
98+
{
99+
while (!_disposed)
100+
{
101+
// watch API requests terminate after 4 minutes
102+
await RunWatcherInternal();
103+
}
104+
}
105+
106+
private async Task RunWatcherInternal()
87107
{
88108
if (string.IsNullOrEmpty(KubernetesObjectName) && FileUtility.DirectoryExists(KubernetesSecretsDir))
89109
{
@@ -124,13 +144,15 @@ private async Task<IDictionary<string, string>> GetFromApiServer(string objectNa
124144
if (response.IsSuccessStatusCode)
125145
{
126146
var obj = await response.Content.ReadAsAsync<JObject>();
127-
return obj["data"]
128-
?.ToObject<IDictionary<string, string>>()
129-
?.ToDictionary(
147+
return obj.ContainsKey("data")
148+
? obj["data"]
149+
.ToObject<IDictionary<string, string>>()
150+
.ToDictionary(
130151
k => k.Key,
131152
v => decode
132153
? Encoding.UTF8.GetString(Convert.FromBase64String(v.Value))
133-
: v.Value);
154+
: v.Value)
155+
: new Dictionary<string, string>();
134156
}
135157
else if (response.StatusCode == HttpStatusCode.NotFound)
136158
{
@@ -139,7 +161,9 @@ private async Task<IDictionary<string, string>> GetFromApiServer(string objectNa
139161
}
140162
else
141163
{
142-
throw new HttpRequestException($"Error calling GET {url}, Status: {response.StatusCode}, Content: {await response.Content.ReadAsStringAsync()}");
164+
var exception = new HttpRequestException($"Error calling GET {url}, Status: {response.StatusCode}, Content: {await response.Content.ReadAsStringAsync()}");
165+
_logger.LogError(exception, "Error reading secrets from Kubernetes");
166+
throw exception;
143167
}
144168
}
145169
}
@@ -165,6 +189,12 @@ private async Task CreateIfDoesntExist(string url, bool isSecret)
165189
using (var createRequest = await GetRequest(HttpMethod.Post, url, payload))
166190
{
167191
var createResponse = await _httpClient.SendAsync(createRequest);
192+
if (!createResponse.IsSuccessStatusCode)
193+
{
194+
_logger.LogError("Error creating Kubernetes secrets. {StatusCode}: {Content}",
195+
createResponse.StatusCode,
196+
await createResponse.Content.ReadAsStringAsync());
197+
}
168198
createResponse.EnsureSuccessStatusCode();
169199
}
170200
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.IO.Abstractions;
7+
using System.Net;
8+
using System.Net.Http;
9+
using System.Net.Http.Headers;
10+
using System.Text;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
using Microsoft.Azure.WebJobs.Script.WebHost;
14+
using Microsoft.Extensions.Logging;
15+
using Microsoft.WebJobs.Script.Tests;
16+
using Moq;
17+
using Moq.Protected;
18+
using Xunit;
19+
20+
namespace Microsoft.Azure.WebJobs.Script.Tests
21+
{
22+
public class SimpleKubernetesClientTests : IDisposable
23+
{
24+
[Theory]
25+
[InlineData(HttpStatusCode.OK, "{}", 0)]
26+
[InlineData(HttpStatusCode.OK, "{'data': {}}", 0)]
27+
[InlineData(HttpStatusCode.OK, "{'data': {'key': 'dmFsdWU='}}", 1)]
28+
public async Task Get_From_ApiServer_No_Data(HttpStatusCode statusCode, string content, int length)
29+
{
30+
var environment = new TestEnvironment();
31+
environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsKubernetesSecretName, "test");
32+
environment.SetEnvironmentVariable(EnvironmentSettingNames.KubernetesServiceHost, "127.0.0.1");
33+
environment.SetEnvironmentVariable(EnvironmentSettingNames.KubernetesServiceHttpsPort, "443");
34+
35+
var fullFileSystem = new FileSystem();
36+
var fileSystem = new Mock<IFileSystem>();
37+
var fileBase = new Mock<FileBase>();
38+
var directoryBase = new Mock<DirectoryBase>();
39+
40+
fileSystem.SetupGet(f => f.Path).Returns(fullFileSystem.Path);
41+
fileSystem.SetupGet(f => f.File).Returns(fileBase.Object);
42+
fileSystem.SetupGet(f => f.Directory).Returns(directoryBase.Object);
43+
fileBase.Setup(f => f.Exists("/run/secrets/kubernetes.io/serviceaccount/namespace")).Returns(true);
44+
fileBase.Setup(f => f.Exists("/run/secrets/kubernetes.io/serviceaccount/token")).Returns(true);
45+
fileBase.Setup(f => f.Exists("/run/secrets/kubernetes.io/serviceaccount/ca.crt")).Returns(true);
46+
47+
fileBase
48+
.Setup(f => f.Open("/run/secrets/kubernetes.io/serviceaccount/token", It.IsAny<FileMode>(), It.IsAny<FileAccess>(), It.IsAny<FileShare>()))
49+
.Returns(() =>
50+
{
51+
var token = new MemoryStream(Encoding.UTF8.GetBytes("test_token"));
52+
token.Position = 0;
53+
return token;
54+
});
55+
fileBase
56+
.Setup(f => f.Open("/run/secrets/kubernetes.io/serviceaccount/namespace", It.IsAny<FileMode>(), It.IsAny<FileAccess>(), It.IsAny<FileShare>()))
57+
.Returns(() =>
58+
{
59+
var ns = new MemoryStream(Encoding.UTF8.GetBytes("namespace"));
60+
ns.Position = 0;
61+
return ns;
62+
});
63+
64+
FileUtility.Instance = fileSystem.Object;
65+
66+
var loggerFactory = new LoggerFactory();
67+
var loggerProvider = new TestLoggerProvider();
68+
loggerFactory.AddProvider(loggerProvider);
69+
70+
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
71+
handlerMock.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()).ReturnsAsync(new HttpResponseMessage
72+
{
73+
StatusCode = statusCode,
74+
75+
Content = new StringContent(content, Encoding.UTF8, "application/json")
76+
});
77+
78+
var client = new SimpleKubernetesClient(environment, new HttpClient(handlerMock.Object), loggerFactory.CreateLogger<SimpleKubernetesClient>());
79+
var secrets = await client.GetSecrets();
80+
81+
Assert.NotNull(secrets);
82+
Assert.Equal(secrets.Count, length);
83+
}
84+
85+
public void Dispose()
86+
{
87+
FileUtility.Instance = null;
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)