Skip to content

Commit 88c87dc

Browse files
Add support for S3 as a remote filesystem (#2676)
1 parent 5cb23a0 commit 88c87dc

35 files changed

+442
-236
lines changed

.env.dev

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ SLOW_PAGE_TIME=1000000
1919
MAIL_MAILER=smtp
2020
MAIL_HOST=mailpit
2121
MAIL_PORT=1025
22+
23+
AWS_ACCESS_KEY_ID=minioadmin
24+
AWS_SECRET_ACCESS_KEY=minioadmin

.env.example

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ DB_PASSWORD=secret
1818
#DB_PORT=
1919
#DB_USERNAME=
2020

21+
# cdash.php
22+
2123
# How long since the last submission before considering a project inactive.
2224
# Set to 0 to always show all projects on viewProjects.php.
2325
#ACTIVE_PROJECT_DAYS=7
@@ -77,6 +79,12 @@ DB_PASSWORD=secret
7779
# something other than an email address in LDAP.
7880
#LOGIN_FIELD=Email
7981

82+
# The maximum visibility level for user-created projects on this instance.
83+
# Instance admins are able to override this setting and set project visibility
84+
# to anything. Thus, this setting is only meaningful if USER_CREATE_PROJECTS=true.
85+
# Options: PUBLIC, PROTECTED, PRIVATE
86+
# MAX_PROJECT_VISIBILITY=PUBLIC
87+
8088
# Maximum per-project upload quota, in GB
8189
#MAX_UPLOAD_QUOTA=10
8290

@@ -148,6 +156,9 @@ DB_PASSWORD=secret
148156
# VCS (eg. GitHub) API endpoints.
149157
#USE_VCS_API=true
150158

159+
# Should normal users be allowed to create projects
160+
# USER_CREATE_PROJECTS = false
161+
151162
# logging.php
152163
#LOG_CHANNEL=stack
153164

@@ -172,6 +183,33 @@ QUEUE_CONNECTION=database
172183
# Number of minutes before and idle session is allowed to expire.
173184
#SESSION_LIFETIME=120
174185

186+
# filesystem.php
187+
188+
# Default filesystem driver for CDash to use.
189+
# Supported options are 'local' and 's3'.
190+
#FILESYSTEM_DRIVER=local
191+
192+
# The following env vars are only relevant for S3 support.
193+
# The name of the bucket that CDash where will store files.
194+
# AWS_BUCKET=cdash
195+
196+
# The AWS region where this S3 bucket is stored.
197+
# Otherwise set this to 'local' if you're using MinIO.
198+
# AWS_REGION=
199+
200+
# Credentials for access to this S3 bucket.
201+
#AWS_ACCESS_KEY_ID=
202+
#AWS_SECRET_ACCESS_KEY=
203+
204+
# Set this to true if you're using MinIO
205+
#AWS_USE_PATH_STYLE_ENDPOINT=false
206+
207+
# URL of your MinIO server (if you're using MinIO). Leave blank otherwise.
208+
#AWS_ENDPOINT=
209+
210+
# URL of the bucket on your MinIO server (if you're using MinIO). Leave blank otherwise.
211+
#AWS_URL=
212+
175213
# mail.php
176214
#MAIL_MAILER=smtp
177215
#MAIL_HOST=smtp.mailgun.org
@@ -258,12 +296,3 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
258296

259297
# Whether or not to automatically register new users upon first login
260298
#SAML2_AUTO_REGISTER_NEW_USERS=false
261-
262-
# Should normal users be allowed to create projects
263-
# USER_CREATE_PROJECTS = false
264-
265-
# The maximum visibility level for user-created projects on this instance.
266-
# Instance admins are able to override this setting and set project visibility
267-
# to anything. Thus, this setting is only meaningful if USER_CREATE_PROJECTS=true.
268-
# Options: PUBLIC, PROTECTED, PRIVATE
269-
# MAX_PROJECT_VISIBILITY=PUBLIC

.github/workflows/ci.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,32 @@ jobs:
1414
env:
1515
SITENAME: GitHub Actions
1616
BASE_IMAGE: ${{matrix.base-image}}
17+
STORAGE_TYPE: ${{matrix.storage}}
1718
runs-on: ubuntu-latest
1819
strategy:
1920
fail-fast: false
2021
matrix:
2122
database: ['mysql', 'postgres']
2223
base-image: ['debian', 'ubi']
24+
storage: ['local', 'minio']
25+
exclude:
26+
- storage: minio
27+
base-image: ubi
28+
- storage: minio
29+
database: mysql
2330
steps:
2431
- uses: actions/checkout@v4
2532
- uses: docker/setup-buildx-action@v3
2633
- name: Build images
2734
shell: bash
2835
run: |
36+
if [ "${{matrix.storage}}" == "minio" ]; then
37+
extra_args="-f docker/docker-compose.minio.yml"
38+
fi
2939
docker compose \
3040
-f docker/docker-compose.yml \
3141
-f docker/docker-compose.dev.yml \
32-
-f "docker/docker-compose.${{matrix.database}}.yml" \
42+
-f "docker/docker-compose.${{matrix.database}}.yml" ${extra_args} \
3343
--env-file .env.dev up -d \
3444
--build \
3545
--wait

.github/workflows/ctest_driver_script.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ ctest_empty_binary_directory("${CTEST_BINARY_DIRECTORY}")
1818
set(cfg_options
1919
"-DCDASH_DIR_NAME="
2020
"-DCDASH_SERVER=localhost:8080"
21+
"-DCDASH_STORAGE_TYPE=${STORAGE_TYPE}"
2122
)
2223

2324
# Backup .env file

.github/workflows/submit.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ submit_type="${submit_type:-Experimental}"
1414

1515
site="${SITENAME:-$(hostname)}"
1616

17+
storage_type="${STORAGE_TYPE:-local}"
18+
1719
echo "site=$site"
1820
echo "database=$database"
1921
echo "ctest_driver=$ctest_driver"
@@ -40,6 +42,7 @@ docker exec cdash bash -c "\
4042
--schedule-random \
4143
-DSITENAME=\"${site}\" \
4244
-DDATABASE=\"${database}\" \
45+
-DSTORAGE_TYPE=\"${storage_type}\" \
4346
-DSUBMIT_TYPE=\"${submit_type}\" \
4447
-S \"${ctest_driver}\" \
4548
"

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ configure_file(
1010
# to configure the testing install
1111
set(CDASH_SERVER localhost CACHE STRING "CDash testing server")
1212
set(CDASH_IMAGE "$ENV{BASE_IMAGE}" CACHE STRING "Docker image name")
13+
if(NOT DEFINED CDASH_STORAGE_TYPE)
14+
set(CDASH_STORAGE_TYPE "local")
15+
endif()
1316

1417
get_filename_component(CDASH_DIR_NAME_DEFAULT ${CDash_SOURCE_DIR} NAME)
1518
set(CDASH_DIR_NAME "${CDASH_DIR_NAME_DEFAULT}" CACHE STRING "URL suffix. Ie 'http://<CDASH_SERVER>/<CDASH_DIR_NAME>'")

app/Console/Commands/ValidateXml.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use App\Utils\SubmissionUtils;
77
use BadMethodCallException;
88
use Illuminate\Console\Command;
9+
use League\Flysystem\UnableToReadFile;
10+
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
911

1012
class ValidateXml extends Command
1113
{
@@ -69,6 +71,9 @@ public function handle(): int
6971
$this->warn("WARNING: Skipped input file '{$input_xml_file}' as validation"
7072
. ' of this file format is currently not supported.');
7173
$has_skipped = true;
74+
} catch (FileNotFoundException|UnableToReadFile $e) {
75+
$this->error($e->getMessage());
76+
$has_skipped = true;
7277
}
7378
}
7479

app/Http/Controllers/BuildController.php

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
use Illuminate\Support\Facades\Log;
3030
use Illuminate\Support\Facades\Storage;
3131
use Illuminate\View\View;
32-
use Symfony\Component\HttpFoundation\BinaryFileResponse;
32+
use Symfony\Component\HttpFoundation\StreamedResponse;
3333

3434
require_once 'include/api_common.php';
3535

@@ -829,7 +829,7 @@ public function files(int $build_id): View
829829
->with('urls', $urls);
830830
}
831831

832-
public function build_file(int $build_id, int $file_id): BinaryFileResponse
832+
public function build_file(int $build_id, int $file_id): StreamedResponse
833833
{
834834
$this->setBuildById($build_id);
835835

@@ -841,10 +841,26 @@ public function build_file(int $build_id, int $file_id): BinaryFileResponse
841841
$uploadFile = new UploadFile();
842842
$uploadFile->Id = $file_id;
843843
$uploadFile->Fill();
844-
return response()->file(Storage::path("upload/{$uploadFile->Sha1Sum}"), [
844+
845+
// The code below satisfies the following requirements:
846+
// 1) Render text and images in browser (as opposed to forcing a download).
847+
// 2) Download other files to the proper filename (not a numeric identifier).
848+
// 3) Support downloading files that are larger than the PHP memory_limit.
849+
$fp = Storage::readStream("upload/{$uploadFile->Sha1Sum}");
850+
if ($fp === null) {
851+
abort(404, 'File not found');
852+
}
853+
$filename = $uploadFile->Filename;
854+
$headers = [
845855
'Content-Type' => 'text/plain',
846856
'Content-Disposition' => "inline/attachment; filename={$uploadFile->Filename}",
847-
]);
857+
];
858+
return response()->streamDownload(function () use ($fp) {
859+
while (!feof($fp)) {
860+
echo fread($fp, 1024);
861+
}
862+
fclose($fp);
863+
}, $filename, $headers, 'inline');
848864
}
849865

850866
public function ajaxBuildNote(): View

app/Http/Controllers/RemoteProcessingController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public function requeueSubmissionFile(): Response
9999
$filename = request()->string('filename');
100100
$buildid = request()->integer('buildid');
101101
$projectid = request()->integer('projectid');
102+
$md5 = request()->string('md5');
102103
if (!Storage::exists("inprogress/{$filename}")) {
103104
return response('File not found', Response::HTTP_NOT_FOUND);
104105
}
@@ -112,7 +113,7 @@ public function requeueSubmissionFile(): Response
112113
// Requeue the file with exponential backoff.
113114
PendingSubmissions::IncrementForBuildId($buildid);
114115
$delay = ((int) config('cdash.retry_base')) ** $retry_handler->Retries;
115-
ProcessSubmission::dispatch($filename, $projectid, $buildid, md5_file(Storage::path("inbox/{$filename}")))->delay(now()->addSeconds($delay));
116+
ProcessSubmission::dispatch($filename, $projectid, $buildid, $md5)->delay(now()->addSeconds($delay));
116117
return response('OK', Response::HTTP_OK);
117118
}
118119
}

app/Http/Controllers/SubmissionController.php

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
use Illuminate\Support\Facades\Log;
2222
use Illuminate\Support\Facades\Storage;
2323
use Illuminate\Support\Str;
24+
use League\Flysystem\UnableToReadFile;
25+
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
2426
use Symfony\Component\HttpKernel\Exception\HttpException;
2527

2628
final class SubmissionController extends AbstractProjectController
@@ -85,23 +87,22 @@ private function submitProcess(): Response
8587
$authtoken = AuthTokenUtil::getBearerToken();
8688
$authtoken_hash = $authtoken === null || $authtoken === '' ? '' : AuthTokenUtil::hashToken($authtoken);
8789

88-
// Save the incoming file in the inbox directory.
89-
$filename = "{$projectname}_-_{$authtoken_hash}_-_" . Str::uuid()->toString() . "_-_{$expected_md5}.xml";
90-
$fp = request()->getContent(true);
91-
if (!Storage::put("inbox/{$filename}", $fp)) {
92-
Log::error("Failed to save submission to inbox for $projectname (md5=$expected_md5)");
93-
abort(Response::HTTP_INTERNAL_SERVER_ERROR, 'Failed to save submission file.');
94-
}
95-
9690
// Check that the md5sum of the file matches what we were told to expect.
91+
$fp = request()->getContent(true);
9792
if (strlen($expected_md5) > 0) {
98-
$md5sum = md5_file(Storage::path("inbox/{$filename}"));
93+
$md5sum = SubmissionUtils::hashFileHandle($fp, 'md5');
9994
if ($md5sum != $expected_md5) {
100-
Storage::delete("inbox/{$filename}");
10195
abort(Response::HTTP_BAD_REQUEST, "md5 mismatch. expected: {$expected_md5}, received: {$md5sum}");
10296
}
10397
}
10498

99+
// Save the incoming file in the inbox directory.
100+
$filename = "{$projectname}_-_{$authtoken_hash}_-_" . Str::uuid()->toString() . "_-_{$expected_md5}.xml";
101+
if (!Storage::put("inbox/{$filename}", $fp)) {
102+
Log::error("Failed to save submission to inbox for $projectname (md5=$expected_md5)");
103+
abort(Response::HTTP_INTERNAL_SERVER_ERROR, 'Failed to save submission file.');
104+
}
105+
105106
// Check if we can connect to the database before proceeding any further.
106107
try {
107108
DB::connection()->getPdo();
@@ -138,7 +139,7 @@ private function submitProcess(): Response
138139
$stored_filename = 'inbox/' . $filename;
139140
$xml_info = [];
140141
try {
141-
$xml_info = SubmissionUtils::get_xml_type(fopen(Storage::path($stored_filename), 'r'), $stored_filename);
142+
$xml_info = SubmissionUtils::get_xml_type(Storage::readStream($stored_filename), $stored_filename);
142143
} catch (BadSubmissionException $e) {
143144
$xml_info['xml_handler'] = '';
144145
$message = "Could not determine submission file type for: '{$stored_filename}'";
@@ -149,7 +150,15 @@ private function submitProcess(): Response
149150
}
150151
if ($xml_info['xml_handler'] !== '') {
151152
// If validation is enabled and if this file has a corresponding schema, validate it
152-
$validation_errors = $xml_info['xml_handler']::validate(storage_path('app/' . $stored_filename));
153+
$validation_errors = [];
154+
try {
155+
$validation_errors = $xml_info['xml_handler']::validate($stored_filename);
156+
} catch (FileNotFoundException|UnableToReadFile $e) {
157+
Log::warning($e->getMessage());
158+
if ((bool) config('cdash.validate_xml_submissions') === true) {
159+
abort(400, "XML validation failed for $filename:" . PHP_EOL . $e->getMessage());
160+
}
161+
}
153162
if (count($validation_errors) > 0) {
154163
$error_string = implode(PHP_EOL, $validation_errors);
155164

@@ -233,18 +242,24 @@ public function storeUploadedFile(Request $request): Response
233242
}
234243

235244
try {
236-
$sha1sum = decrypt($request->input('sha1sum'));
245+
$expected_sha1sum = decrypt($request->input('sha1sum'));
237246
} catch (DecryptException $e) {
238247
return response('This feature is disabled', Response::HTTP_CONFLICT);
239248
}
240249

241250
$uploaded_file = array_values(request()->allFiles())[0];
242-
$stored_path = $uploaded_file->storeAs('upload', $sha1sum);
251+
$stored_path = $uploaded_file->storeAs('upload', $expected_sha1sum);
243252
if ($stored_path === false) {
244253
abort(Response::HTTP_INTERNAL_SERVER_ERROR, 'Failed to store uploaded file');
245254
}
246255

247-
if (sha1_file(Storage::path($stored_path)) !== $sha1sum) {
256+
$fp = Storage::readStream($stored_path);
257+
if ($fp === null) {
258+
abort(Response::HTTP_INTERNAL_SERVER_ERROR, 'Failed to store uploaded file');
259+
}
260+
261+
$found_sha1sum = SubmissionUtils::hashFileHandle($fp, 'sha1');
262+
if ($found_sha1sum !== $expected_sha1sum) {
248263
Storage::delete($stored_path);
249264
return response('Uploaded file does not match expected sha1sum', Response::HTTP_BAD_REQUEST);
250265
}

0 commit comments

Comments
 (0)