Skip to content

Commit f2478de

Browse files
authoredJun 1, 2023
feat: use pre-downloaded binaries for cfssl (#568)
This PR makes sure that the init pod will use pre-downloaded binaries, so that the operator can be deployed in offline scenarios. Also it no longer makes the assumption that binaries like `sh` and `chmod` exist in the docker image (a lot of hardened/ minimal images don't have shell on them). Other changes: - modified `server.pem` and `server-key.pem` to `ca.pem` and `ca-key.pem` - AFAICT, these are the file names generated by cfssljson ; yes I know it's confusing but these are different than the input ones. - passed additional argument "-r" to the init pod - we don't want it failing on subsequent deployments/ if a webhook already exists. Fixes #567.
1 parent 0342b8c commit f2478de

File tree

7 files changed

+69
-104
lines changed

7 files changed

+69
-104
lines changed
 

‎src/KubeOps.Testing/KubernetesOperatorFactory.cs

+7
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ public KubernetesOperatorFactory<TTestStartup> WithSolutionRelativeContentRoot(s
4242
return this;
4343
}
4444

45+
public IManagedResourceController GetController<TEntity>()
46+
where TEntity : IKubernetesObject<V1ObjectMeta> =>
47+
Services
48+
.GetRequiredService<IControllerInstanceBuilder>()
49+
.BuildControllers<TEntity>()
50+
.First();
51+
4552
/// <summary>
4653
/// Start the server.
4754
/// </summary>

‎src/KubeOps/Operator/Commands/CommandHelpers/CertificateGenerator.cs

+50-95
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
using System.Diagnostics;
2-
using System.Runtime.InteropServices;
3-
using McMaster.Extensions.CommandLineUtils;
42

53
namespace KubeOps.Operator.Commands.CommandHelpers;
64

75
internal class CertificateGenerator : IDisposable
86
{
7+
/* Suggested URLs for downloading the executables into the docker image
98
private const string CfsslUrlWindows =
109
"https://github.com/cloudflare/cfssl/releases/download/v1.5.0/cfssl_1.5.0_windows_amd64.exe";
1110
@@ -23,6 +22,7 @@ internal class CertificateGenerator : IDisposable
2322
2423
private const string CfsslJsonUrlMacOs =
2524
"https://github.com/cloudflare/cfssl/releases/download/v1.5.0/cfssljson_1.5.0_darwin_amd64";
25+
*/
2626

2727
private const string CaConfig =
2828
@"{""signing"":{""default"":{""expiry"":""43800h""},""profiles"":{""server"":{""expiry"":""43800h"",""usages"":[""signing"",""key encipherment"",""server auth""]}}}}";
@@ -34,21 +34,17 @@ internal class CertificateGenerator : IDisposable
3434
private readonly string _tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
3535

3636
private bool _initialized;
37-
private string? _cfssl;
38-
private string? _cfssljson;
3937
private string? _caconfig;
4038
private string? _cacsr;
4139
private string? _servercsr;
40+
private string _cfssl = "cfssl";
41+
private string _cfssljson = "cfssljson";
4242

4343
public CertificateGenerator(TextWriter appOut)
4444
{
4545
_appOut = appOut;
4646
}
4747

48-
private static string ShellExecutor => RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
49-
? "cmd.exe"
50-
: "/bin/sh";
51-
5248
public void Dispose()
5349
{
5450
Delete(_caconfig);
@@ -62,6 +58,9 @@ public async Task CreateCaCertificateAsync(string outputFolder)
6258
{
6359
if (!_initialized)
6460
{
61+
var cfsslFolder = Environment.GetEnvironmentVariable("CFSSL_EXECUTABLES_PATH") ?? "/operator";
62+
_cfssl = $"{cfsslFolder}/{_cfssl}";
63+
_cfssljson = $"{cfsslFolder}/{_cfssljson}";
6564
await PrepareExecutables();
6665
_initialized = true;
6766
}
@@ -70,18 +69,8 @@ public async Task CreateCaCertificateAsync(string outputFolder)
7069

7170
await _appOut.WriteLineAsync($@"Generating certificates to ""{outputFolder}"".");
7271
await _appOut.WriteLineAsync("Generating CA certificate.");
73-
await ExecuteProcess(
74-
new Process
75-
{
76-
StartInfo = new ProcessStartInfo
77-
{
78-
WorkingDirectory = outputFolder,
79-
FileName = ShellExecutor,
80-
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
81-
? $"/c {_cfssl} gencert -initca {_cacsr} | {_cfssljson} -bare ca -"
82-
: $@"-c ""{_cfssl} gencert -initca {_cacsr} | {_cfssljson} -bare ca -""",
83-
},
84-
});
72+
73+
await GenCertAsync($"-initca {_cacsr}", outputFolder);
8574

8675
await ListDir(outputFolder);
8776
}
@@ -110,33 +99,15 @@ await serverCsrStream.WriteLineAsync(
11099

111100
await _appOut.WriteLineAsync(
112101
$@"Generating server certificate for ""{name}"" in namespace ""{@namespace}"".");
113-
await ExecuteProcess(
114-
new Process
115-
{
116-
StartInfo = new ProcessStartInfo
117-
{
118-
WorkingDirectory = outputFolder,
119-
FileName = ShellExecutor,
120-
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
121-
? $"/c {_cfssl} gencert -ca=file:{caPath.Replace('\\', '/')} -ca-key=file:{caKeyPath.Replace('\\', '/')} -config={_caconfig} -profile=server {_servercsr} | {_cfssljson} -bare server -"
122-
: $@"-c ""{_cfssl} gencert -ca={caPath} -ca-key={caKeyPath} -config={_caconfig} -profile=server {_servercsr} | {_cfssljson} -bare server -""",
123-
},
124-
});
125-
102+
var arguments =
103+
$"-ca=file:{caPath.Replace('\\', '/')} -ca-key=file:{caKeyPath.Replace('\\', '/')} -config={_caconfig} -profile=server {_servercsr}";
104+
await GenCertAsync(arguments, outputFolder);
126105
await ListDir(outputFolder);
127106
}
128107

129108
private static string ServerCsr(params string[] serverNames) =>
130109
$@"{{""CN"":""Operator Service"",""hosts"":[""{string.Join(@""",""", serverNames)}""],""key"":{{""algo"":""ecdsa"",""size"":256}},""names"":[{{""C"":""DEV"",""L"":""Kubernetes""}}]}}";
131110

132-
private static Task ExecuteProcess(Process process)
133-
=> Task.Run(
134-
() =>
135-
{
136-
process.Start();
137-
process.WaitForExit(2000);
138-
});
139-
140111
private static void Delete(string? file)
141112
{
142113
if (file == null)
@@ -150,66 +121,50 @@ private static void Delete(string? file)
150121
}
151122
}
152123

124+
private async Task GenCertAsync(
125+
string arguments,
126+
string outputFolder,
127+
CancellationToken cancellationToken = default)
128+
{
129+
var genCertPsi = new ProcessStartInfo
130+
{
131+
WorkingDirectory = outputFolder,
132+
FileName = _cfssl,
133+
Arguments = $"gencert {arguments}",
134+
RedirectStandardOutput = true,
135+
UseShellExecute = false,
136+
};
137+
var writeJsonPsi = new ProcessStartInfo
138+
{
139+
WorkingDirectory = outputFolder,
140+
FileName = _cfssljson,
141+
Arguments = "-bare ca -",
142+
RedirectStandardInput = true,
143+
UseShellExecute = false,
144+
};
145+
using var genCert = new Process { StartInfo = genCertPsi };
146+
using var writeJson = new Process { StartInfo = writeJsonPsi };
147+
148+
genCert.Start();
149+
await genCert.WaitForExitAsync(cancellationToken);
150+
151+
writeJson.Start();
152+
await writeJson.StandardInput.WriteAsync(await genCert.StandardOutput.ReadToEndAsync());
153+
await writeJson.StandardInput.FlushAsync();
154+
writeJson.StandardInput.Close();
155+
await writeJson.WaitForExitAsync(cancellationToken);
156+
}
157+
153158
private async Task PrepareExecutables()
154159
{
155160
Directory.CreateDirectory(_tempDirectory);
156-
_cfssl = Path.Join(_tempDirectory, Path.GetRandomFileName());
157-
_cfssljson = Path.Join(_tempDirectory, Path.GetRandomFileName());
158161
_caconfig = Path.Join(_tempDirectory, Path.GetRandomFileName());
159162
_cacsr = Path.Join(_tempDirectory, Path.GetRandomFileName());
160163

161-
using (var client = new HttpClient())
162-
await using (var cfsslStream = new FileStream(_cfssl, FileMode.CreateNew))
163-
await using (var cfsslJsonStream = new FileStream(_cfssljson, FileMode.CreateNew))
164-
await using (var caConfigStream = new StreamWriter(new FileStream(_caconfig, FileMode.CreateNew)))
165-
await using (var caCsrStream = new StreamWriter(new FileStream(_cacsr, FileMode.CreateNew)))
166-
{
167-
await caConfigStream.WriteLineAsync(CaConfig);
168-
await caCsrStream.WriteLineAsync(CaCsr);
169-
170-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
171-
{
172-
await _appOut.WriteLineAsync("Download cfssl / cfssljson for windows.");
173-
await using var cfsslDl = await client.GetStreamAsync(CfsslUrlWindows);
174-
await using var cfsslJsonDl = await client.GetStreamAsync(CfsslJsonUrlWindows);
175-
176-
await cfsslDl.CopyToAsync(cfsslStream);
177-
await cfsslJsonDl.CopyToAsync(cfsslJsonStream);
178-
}
179-
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
180-
{
181-
await _appOut.WriteLineAsync("Download cfssl / cfssljson for linux.");
182-
await using var cfsslDl = await client.GetStreamAsync(CfsslUrlLinux);
183-
await using var cfsslJsonDl = await client.GetStreamAsync(CfsslJsonUrlLinux);
184-
185-
await cfsslDl.CopyToAsync(cfsslStream);
186-
await cfsslJsonDl.CopyToAsync(cfsslJsonStream);
187-
}
188-
else
189-
{
190-
await _appOut.WriteLineAsync("Download cfssl / cfssljson for macos.");
191-
await using var cfsslDl = await client.GetStreamAsync(CfsslUrlMacOs);
192-
await using var cfsslJsonDl = await client.GetStreamAsync(CfsslJsonUrlMacOs);
193-
194-
await cfsslDl.CopyToAsync(cfsslStream);
195-
await cfsslJsonDl.CopyToAsync(cfsslJsonStream);
196-
}
197-
}
198-
199-
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
200-
{
201-
await _appOut.WriteLineAsync("Make unix binaries executable.");
202-
await ExecuteProcess(
203-
new Process
204-
{
205-
StartInfo = new ProcessStartInfo
206-
{
207-
FileName = "chmod",
208-
Arguments = ArgumentEscaper.EscapeAndConcatenate(
209-
new[] { "+x", _cfssl, _cfssljson }),
210-
},
211-
});
212-
}
164+
await using var caConfigStream = new StreamWriter(new FileStream(_caconfig, FileMode.CreateNew));
165+
await using var caCsrStream = new StreamWriter(new FileStream(_cacsr, FileMode.CreateNew));
166+
await caConfigStream.WriteLineAsync(CaConfig);
167+
await caCsrStream.WriteLineAsync(CaCsr);
213168
}
214169

215170
private async Task ListDir(string directory)

‎src/KubeOps/Operator/Commands/Generators/DockerGenerator.cs

+6
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ public string GenerateDockerfile() =>
5656
WORKDIR /operator
5757
5858
COPY ./ ./
59+
RUN curl -L -o cfssl https://github.com/cloudflare/cfssl/releases/download/v1.5.0/cfssl_1.5.0_linux_amd64
60+
RUN curl -L -o cfssljson https://github.com/cloudflare/cfssl/releases/download/v1.5.0/cfssljson_1.5.0_linux_amd64
61+
RUN chmod +x ./cfssl
62+
RUN chmod +x ./cfssljson
63+
RUN mkdir out
64+
RUN cp cfssl cfssljson out/
5965
RUN dotnet publish -c Release -o out {ProjectToBuild}
6066
6167
# The runner for the application

‎src/KubeOps/Operator/Commands/Generators/OperatorGenerator.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ public async Task<int> OnExecuteAsync(CommandLineApplication app)
5353
{
5454
$"KESTREL__ENDPOINTS__HTTP__URL=http://0.0.0.0:{_settings.HttpPort}",
5555
$"KESTREL__ENDPOINTS__HTTPS__URL=https://0.0.0.0:{_settings.HttpsPort}",
56-
"KESTREL__ENDPOINTS__HTTPS__CERTIFICATE__PATH=/certs/server.pem",
57-
"KESTREL__ENDPOINTS__HTTPS__CERTIFICATE__KEYPATH=/certs/server-key.pem",
56+
"KESTREL__ENDPOINTS__HTTPS__CERTIFICATE__PATH=/certs/ca.pem",
57+
"KESTREL__ENDPOINTS__HTTPS__CERTIFICATE__KEYPATH=/certs/ca-key.pem",
5858
},
5959
},
6060
}
@@ -112,7 +112,7 @@ public async Task<int> OnExecuteAsync(CommandLineApplication app)
112112
{
113113
Image = "operator",
114114
Name = "webhook-installer",
115-
Args = new[] { "webhooks", "install", },
115+
Args = new[] { "webhooks", "install", "-r" },
116116
Env = new List<V1EnvVar>
117117
{
118118
new()

‎src/KubeOps/Operator/Commands/Management/Webhooks/Install.cs

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ await client.Create(
131131
if (existingItem != null)
132132
{
133133
await app.Out.WriteLineAsync("Validator existed, updating.");
134+
existingItem.Webhooks = validatorConfig.Webhooks;
134135
await client.Update(existingItem);
135136
}
136137
else

‎src/KubeOps/Operator/Controller/IManagedResourceController.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace KubeOps.Operator.Controller;
22

3-
internal interface IManagedResourceController : IDisposable
3+
public interface IManagedResourceController : IDisposable
44
{
55
Task StartAsync();
66

‎tests/KubeOps.TestOperator.Test/TestController.Test.cs

+1-5
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,7 @@ public TestControllerTest(KubernetesOperatorFactory<TestStartup> factory)
2020
{
2121
_factory = factory.WithSolutionRelativeContentRoot("tests/KubeOps.TestOperator");
2222

23-
_controller = _factory.Services
24-
.GetRequiredService<IControllerInstanceBuilder>()
25-
.BuildControllers<V1TestEntity>()
26-
.First();
27-
23+
_controller = _factory.GetController<V1TestEntity>();
2824
_managerMock = _factory.Services.GetRequiredService<Mock<IManager>>();
2925
_managerMock.Reset();
3026
}

0 commit comments

Comments
 (0)
Please sign in to comment.