12
12
use Exception ;
13
13
use GuzzleHttp \Client ;
14
14
use GuzzleHttp \Exception \GuzzleException ;
15
-
16
15
use OCA \AppAPI \AppInfo \Application ;
17
16
use OCA \AppAPI \Db \DaemonConfig ;
17
+
18
18
use OCA \AppAPI \Db \ExApp ;
19
19
use OCA \AppAPI \Service \AppAPICommonService ;
20
-
21
20
use OCA \AppAPI \Service \ExAppService ;
22
21
use OCP \App \IAppManager ;
22
+
23
23
use OCP \ICertificateManager ;
24
24
use OCP \IConfig ;
25
+ use OCP \ITempManager ;
25
26
use OCP \IURLGenerator ;
26
27
use OCP \Security \ICrypto ;
28
+ use Phar ;
29
+ use PharData ;
27
30
use Psr \Log \LoggerInterface ;
28
31
29
32
class DockerActions implements IDeployActions {
@@ -42,6 +45,7 @@ public function __construct(
42
45
private readonly IURLGenerator $ urlGenerator ,
43
46
private readonly AppAPICommonService $ service ,
44
47
private readonly ExAppService $ exAppService ,
48
+ private readonly ITempManager $ tempManager ,
45
49
private readonly ICrypto $ crypto ,
46
50
) {
47
51
}
@@ -69,9 +73,10 @@ public function deployExApp(ExApp $exApp, DaemonConfig $daemonConfig, array $par
69
73
}
70
74
71
75
$ this ->exAppService ->setAppDeployProgress ($ exApp , 95 );
72
- $ containerInfo = $ this ->inspectContainer ($ dockerUrl , $ this ->buildExAppContainerName ($ params ['container_params ' ]['name ' ]));
76
+ $ containerName = $ this ->buildExAppContainerName ($ params ['container_params ' ]['name ' ]);
77
+ $ containerInfo = $ this ->inspectContainer ($ dockerUrl , $ containerName );
73
78
if (isset ($ containerInfo ['Id ' ])) {
74
- $ result = $ this ->removeContainer ($ dockerUrl , $ this -> buildExAppContainerName ( $ params [ ' container_params ' ][ ' name ' ]) );
79
+ $ result = $ this ->removeContainer ($ dockerUrl , $ containerName );
75
80
if ($ result ) {
76
81
return $ result ;
77
82
}
@@ -82,18 +87,176 @@ public function deployExApp(ExApp $exApp, DaemonConfig $daemonConfig, array $par
82
87
return $ result ['error ' ];
83
88
}
84
89
$ this ->exAppService ->setAppDeployProgress ($ exApp , 97 );
85
- $ result = $ this ->startContainer ($ dockerUrl , $ this ->buildExAppContainerName ($ params ['container_params ' ]['name ' ]));
90
+
91
+ $ this ->updateCerts ($ dockerUrl , $ containerName );
92
+ $ this ->exAppService ->setAppDeployProgress ($ exApp , 98 );
93
+
94
+ $ result = $ this ->startContainer ($ dockerUrl , $ containerName );
86
95
if (isset ($ result ['error ' ])) {
87
96
return $ result ['error ' ];
88
97
}
89
98
$ this ->exAppService ->setAppDeployProgress ($ exApp , 99 );
90
- if (!$ this ->waitTillContainerStart ($ this -> buildExAppContainerName ( $ exApp -> getAppid ()) , $ daemonConfig )) {
99
+ if (!$ this ->waitTillContainerStart ($ containerName , $ daemonConfig )) {
91
100
return 'container startup failed ' ;
92
101
}
93
102
$ this ->exAppService ->setAppDeployProgress ($ exApp , 100 );
94
103
return '' ;
95
104
}
96
105
106
+ private function updateCerts (string $ dockerUrl , string $ containerName ): void {
107
+ try {
108
+ $ this ->startContainer ($ dockerUrl , $ containerName );
109
+
110
+ $ osInfo = $ this ->getContainerOsInfo ($ dockerUrl , $ containerName );
111
+ if (!$ this ->isSupportedOs ($ osInfo )) {
112
+ $ this ->logger ->warning (sprintf (
113
+ "Unsupported OS detected for container: %s. OS info: %s " ,
114
+ $ containerName ,
115
+ $ osInfo
116
+ ));
117
+ return ;
118
+ }
119
+
120
+ $ bundlePath = $ this ->certificateManager ->getAbsoluteBundlePath ();
121
+ $ targetDir = $ this ->getTargetCertDir ($ osInfo ); // Determine target directory based on OS
122
+ $ this ->executeCommandInContainer ($ dockerUrl , $ containerName , ['mkdir ' , '-p ' , $ targetDir ]);
123
+ $ this ->installParsedCertificates ($ dockerUrl , $ containerName , $ bundlePath , $ targetDir );
124
+
125
+ $ updateCommand = $ this ->getCertificateUpdateCommand ($ osInfo );
126
+ $ this ->executeCommandInContainer ($ dockerUrl , $ containerName , $ updateCommand );
127
+ } catch (Exception $ e ) {
128
+ $ this ->logger ->warning (sprintf (
129
+ "Failed to update certificates in container: %s. Error: %s " ,
130
+ $ containerName ,
131
+ $ e ->getMessage ()
132
+ ));
133
+ } finally {
134
+ $ this ->stopContainer ($ dockerUrl , $ containerName );
135
+ }
136
+ }
137
+
138
+ private function parseCertificatesFromBundle (string $ bundlePath ): array {
139
+ $ contents = file_get_contents ($ bundlePath );
140
+
141
+ // Match only certificates
142
+ preg_match_all ('/-----BEGIN CERTIFICATE-----(.+?)-----END CERTIFICATE-----/s ' , $ contents , $ matches );
143
+
144
+ return $ matches [0 ] ?? [];
145
+ }
146
+
147
+ private function installParsedCertificates (string $ dockerUrl , string $ containerId , string $ bundlePath , string $ targetDir ): void {
148
+ $ certificates = $ this ->parseCertificatesFromBundle ($ bundlePath );
149
+ $ tempDir = sys_get_temp_dir ();
150
+
151
+ foreach ($ certificates as $ index => $ certificate ) {
152
+ $ tempFile = $ tempDir . "/ {$ containerId }_cert_ {$ index }.crt " ;
153
+ if (file_exists ($ tempFile )) {
154
+ unlink ($ tempFile );
155
+ }
156
+ file_put_contents ($ tempFile , $ certificate );
157
+
158
+ // Build the path in the container
159
+ $ pathInContainer = $ targetDir . "/custom_cert_ $ index.crt " ;
160
+
161
+ $ this ->dockerCopy ($ dockerUrl , $ containerId , $ tempFile , $ pathInContainer );
162
+ unlink ($ tempFile );
163
+ }
164
+ }
165
+
166
+ private function dockerCopy (string $ dockerUrl , string $ containerId , string $ sourcePath , string $ pathInContainer ): void {
167
+ $ archivePath = $ this ->createTarArchive ($ sourcePath , $ pathInContainer );
168
+ $ url = $ this ->buildApiUrl ($ dockerUrl , sprintf ('containers/%s/archive?path=%s ' , $ containerId , urlencode ('/ ' )));
169
+
170
+ try {
171
+ $ archiveData = file_get_contents ($ archivePath );
172
+ $ this ->guzzleClient ->put ($ url , [
173
+ 'body ' => $ archiveData ,
174
+ 'headers ' => ['Content-Type ' => 'application/x-tar ' ]
175
+ ]);
176
+ } catch (Exception $ e ) {
177
+ throw new Exception (sprintf ("Failed to copy %s to container %s: %s " , $ sourcePath , $ containerId , $ e ->getMessage ()));
178
+ } finally {
179
+ if (file_exists ($ archivePath )) {
180
+ unlink ($ archivePath );
181
+ }
182
+ }
183
+ }
184
+
185
+ private function getTargetCertDir (string $ osInfo ): string {
186
+ if (stripos ($ osInfo , 'alpine ' ) !== false ) {
187
+ return '/usr/local/share/ca-certificates ' ; // Alpine Linux
188
+ }
189
+
190
+ if (stripos ($ osInfo , 'debian ' ) !== false || stripos ($ osInfo , 'ubuntu ' ) !== false ) {
191
+ return '/usr/local/share/ca-certificates ' ; // Debian and Ubuntu
192
+ }
193
+
194
+ if (stripos ($ osInfo , 'centos ' ) !== false || stripos ($ osInfo , 'almalinux ' ) !== false ) {
195
+ return '/etc/pki/ca-trust/source/anchors ' ; // CentOS and AlmaLinux
196
+ }
197
+
198
+ throw new Exception (sprintf ('Unsupported OS: %s ' , $ osInfo ));
199
+ }
200
+
201
+ private function createTarArchive (string $ filePath , string $ pathInContainer ): string {
202
+ $ tempFile = $ this ->tempManager ->getTemporaryFile ('.tar ' );
203
+
204
+ try {
205
+ if (file_exists ($ tempFile )) {
206
+ unlink ($ tempFile );
207
+ }
208
+
209
+ $ archive = new PharData ($ tempFile , 0 , null , Phar::TAR );
210
+ $ relativePathInArchive = ltrim ($ pathInContainer , '/ ' );
211
+ $ archive ->addFile ($ filePath , $ relativePathInArchive );
212
+ } catch (\Exception $ e ) {
213
+ // Clean up the temporary file in case of an error
214
+ if (file_exists ($ tempFile )) {
215
+ unlink ($ tempFile );
216
+ }
217
+ throw new Exception (sprintf ("Failed to create tar archive: %s " , $ e ->getMessage ()));
218
+ }
219
+ return $ tempFile ; // Return the path to the TAR archive
220
+ }
221
+
222
+ private function getCertificateUpdateCommand (string $ osInfo ): string {
223
+ if (stripos ($ osInfo , 'alpine ' ) !== false ) {
224
+ return 'update-ca-certificates ' ;
225
+ }
226
+ if (stripos ($ osInfo , 'debian ' ) !== false || stripos ($ osInfo , 'ubuntu ' ) !== false ) {
227
+ return 'update-ca-certificates ' ;
228
+ }
229
+ if (stripos ($ osInfo , 'centos ' ) !== false || stripos ($ osInfo , 'almalinux ' ) !== false ) {
230
+ return 'update-ca-trust extract ' ;
231
+ }
232
+ throw new Exception ('Unsupported OS ' );
233
+ }
234
+
235
+ private function getContainerOsInfo (string $ dockerUrl , string $ containerId ): string {
236
+ $ command = ['cat ' , '/etc/os-release ' ];
237
+ return $ this ->executeCommandInContainer ($ dockerUrl , $ containerId , $ command );
238
+ }
239
+
240
+ private function isSupportedOs (string $ osInfo ): bool {
241
+ return (bool ) preg_match ('/(alpine|debian|ubuntu|centos|almalinux)/i ' , $ osInfo );
242
+ }
243
+
244
+ private function executeCommandInContainer (string $ dockerUrl , string $ containerId , $ command ): string {
245
+ $ url = $ this ->buildApiUrl ($ dockerUrl , sprintf ('containers/%s/exec ' , $ containerId ));
246
+ $ payload = [
247
+ 'Cmd ' => is_array ($ command ) ? $ command : explode (' ' , $ command ),
248
+ 'AttachStdout ' => true ,
249
+ 'AttachStderr ' => true ,
250
+ ];
251
+ $ response = $ this ->guzzleClient ->post ($ url , ['json ' => $ payload ]);
252
+ $ execId = json_decode ((string ) $ response ->getBody (), true )['Id ' ];
253
+
254
+ // Start the exec process
255
+ $ startUrl = $ this ->buildApiUrl ($ dockerUrl , sprintf ('exec/%s/start ' , $ execId ));
256
+ $ startResponse = $ this ->guzzleClient ->post ($ startUrl , ['json ' => ['Detach ' => false , 'Tty ' => false ]]);
257
+ return (string ) $ startResponse ->getBody ();
258
+ }
259
+
97
260
public function buildApiUrl (string $ dockerUrl , string $ route ): string {
98
261
return sprintf ('%s/%s/%s ' , $ dockerUrl , self ::DOCKER_API_VERSION , $ route );
99
262
}
0 commit comments