Skip to content

Commit 59c905a

Browse files
authored
Use azcopy for uploading large artifacts (#1679)
1 parent 734ea2b commit 59c905a

File tree

8 files changed

+209
-6
lines changed

8 files changed

+209
-6
lines changed

include/vcpkg/base/downloads.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ namespace vcpkg
116116
const Path& file_to_put,
117117
StringView sha512);
118118

119+
bool azcopy_to_asset_cache(DiagnosticContext& context,
120+
StringView raw_url,
121+
const SanitizedUrl& sanitized_url,
122+
const Path& file);
123+
119124
Optional<unsigned long long> try_parse_curl_max5_size(StringView sv);
120125

121126
struct CurlProgressData

include/vcpkg/base/message-data.inc.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,10 @@ DECLARE_MESSAGE(AVersionDatabaseEntry, (), "", "a version database entry")
327327
DECLARE_MESSAGE(AVersionObject, (), "", "a version object")
328328
DECLARE_MESSAGE(AVersionOfAnyType, (), "", "a version of any type")
329329
DECLARE_MESSAGE(AVersionConstraint, (), "", "a version constraint")
330+
DECLARE_MESSAGE(AzcopyFailedToPutBlob,
331+
(msg::exit_code, msg::url, msg::value),
332+
"azcopy is the name of a program. {value} is an HTTP status code.",
333+
"azcopy failed to upload a file to {url} with exit code {exit_code} and http code {value}.")
330334
DECLARE_MESSAGE(AzUrlAssetCacheRequiresBaseUrl,
331335
(),
332336
"",

include/vcpkg/binarycaching.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ namespace vcpkg
148148
std::vector<UrlTemplate> url_templates_to_get;
149149
std::vector<UrlTemplate> url_templates_to_put;
150150

151+
std::vector<UrlTemplate> azblob_templates_to_put;
152+
151153
std::vector<std::string> gcs_read_prefixes;
152154
std::vector<std::string> gcs_write_prefixes;
153155

locales/messages.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@
211211
"AvailableHelpTopics": "Available help topics:",
212212
"AzUrlAssetCacheRequiresBaseUrl": "unexpected arguments: asset config 'azurl' requires a base url",
213213
"AzUrlAssetCacheRequiresLessThanFour": "unexpected arguments: asset config 'azurl' requires fewer than 4 arguments",
214+
"AzcopyFailedToPutBlob": "azcopy failed to upload a file to {url} with exit code {exit_code} and http code {value}.",
215+
"_AzcopyFailedToPutBlob.comment": "azcopy is the name of a program. {value} is an HTTP status code. An example of {exit_code} is 127. An example of {url} is https://github.com/microsoft/vcpkg.",
214216
"BaselineConflict": "Specifying vcpkg-configuration.default-registry in a manifest file conflicts with built-in baseline.\nPlease remove one of these conflicting settings.",
215217
"BaselineGitShowFailed": "while checking out baseline from commit '{commit_sha}', failed to `git show` versions/baseline.json. This may be fixed by fetching commits with `git fetch`.",
216218
"_BaselineGitShowFailed.comment": "An example of {commit_sha} is 7cfad47ae9f68b183983090afd6337cd60fd4949.",

src/vcpkg-test/configparser.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -459,8 +459,9 @@ TEST_CASE ("BinaryConfigParser azblob provider", "[binaryconfigparser]")
459459

460460
REQUIRE(state.binary_cache_providers == std::set<StringLiteral>{{"azblob"}, {"default"}});
461461
CHECK(state.url_templates_to_get.empty());
462-
CHECK(state.url_templates_to_put.size() == 1);
463-
CHECK(state.url_templates_to_put.front().url_template == "https://azure/container/{sha}.zip?sas");
462+
CHECK(state.url_templates_to_put.empty());
463+
CHECK(state.azblob_templates_to_put.size() == 1);
464+
CHECK(state.azblob_templates_to_put.front().url_template == "https://azure/container/{sha}.zip?sas");
464465
REQUIRE(state.secrets == std::vector<std::string>{"sas"});
465466
REQUIRE(!state.archives_to_write.empty());
466467
}
@@ -471,8 +472,9 @@ TEST_CASE ("BinaryConfigParser azblob provider", "[binaryconfigparser]")
471472
REQUIRE(state.binary_cache_providers == std::set<StringLiteral>{{"azblob"}, {"default"}});
472473
CHECK(state.url_templates_to_get.size() == 1);
473474
CHECK(state.url_templates_to_get.front().url_template == "https://azure/container/{sha}.zip?sas");
474-
CHECK(state.url_templates_to_put.size() == 1);
475-
CHECK(state.url_templates_to_put.front().url_template == "https://azure/container/{sha}.zip?sas");
475+
CHECK(state.url_templates_to_put.empty());
476+
CHECK(state.azblob_templates_to_put.size() == 1);
477+
CHECK(state.azblob_templates_to_put.front().url_template == "https://azure/container/{sha}.zip?sas");
476478
REQUIRE(state.secrets == std::vector<std::string>{"sas"});
477479
REQUIRE(!state.archives_to_read.empty());
478480
REQUIRE(!state.archives_to_write.empty());

src/vcpkg-test/downloads.cpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,22 @@
22

33
#include <vcpkg/base/downloads.h>
44
#include <vcpkg/base/expected.h>
5+
#include <vcpkg/base/system.h>
6+
#include <vcpkg/base/util.h>
7+
8+
#include <random>
59

610
using namespace vcpkg;
711

12+
#define CHECK_EC_ON_FILE(file, ec) \
13+
do \
14+
{ \
15+
if (ec) \
16+
{ \
17+
FAIL((file).native() << ": " << (ec).message()); \
18+
} \
19+
} while (0)
20+
821
TEST_CASE ("parse_split_url_view", "[downloads]")
922
{
1023
{
@@ -313,3 +326,83 @@ TEST_CASE ("url_encode_spaces", "[downloads]")
313326
REQUIRE(url_encode_spaces("https://example.com/a space/b?query=value&query2=value2") ==
314327
"https://example.com/a%20%20space/b?query=value&query2=value2");
315328
}
329+
330+
/*
331+
* To run this test:
332+
* - Set environment variables VCPKG_TEST_AZBLOB_URL and VCPKG_TEST_AZBLOB_SAS.
333+
* (Use Azurite for creating a local test environment, and
334+
* Azure Storage Explorer for getting a suitable Shared Access Signature.)
335+
* - Run 'vcpkg-test azblob [-s]'.
336+
*/
337+
TEST_CASE ("azblob", "[.][azblob]")
338+
{
339+
auto maybe_url = vcpkg::get_environment_variable("VCPKG_TEST_AZBLOB_URL");
340+
REQUIRE(maybe_url.has_value());
341+
std::string url = maybe_url.value_or_exit(VCPKG_LINE_INFO);
342+
REQUIRE(!url.empty());
343+
344+
if (url.back() != '/') url += '/';
345+
346+
auto maybe_sas = vcpkg::get_environment_variable("VCPKG_TEST_AZBLOB_SAS");
347+
REQUIRE(maybe_sas.has_value());
348+
std::string query_string = maybe_sas.value_or_exit(VCPKG_LINE_INFO);
349+
REQUIRE(!query_string.empty());
350+
351+
if (query_string.front() != '?') query_string += '?' + query_string;
352+
353+
auto& fs = real_filesystem;
354+
auto temp_dir = Test::base_temporary_directory() / "azblob";
355+
fs.remove_all(temp_dir, VCPKG_LINE_INFO);
356+
357+
std::error_code ec;
358+
fs.create_directories(temp_dir, ec);
359+
CHECK_EC_ON_FILE(temp_dir, ec);
360+
361+
const char* data = "(blob content)";
362+
auto data_filepath = temp_dir / "data";
363+
CAPTURE(data_filepath);
364+
fs.write_contents(data_filepath, data, ec);
365+
CHECK_EC_ON_FILE(data_filepath, ec);
366+
367+
auto rnd = Strings::b32_encode(std::mt19937_64()());
368+
std::vector<std::pair<std::string, Path>> url_pairs;
369+
{
370+
auto plain_put_filename = "plain_put_" + rnd;
371+
auto plain_put_url = url + plain_put_filename + query_string;
372+
url_pairs.emplace_back(plain_put_url, temp_dir / plain_put_filename);
373+
374+
FullyBufferedDiagnosticContext diagnostics{};
375+
auto plain_put_success = store_to_asset_cache(
376+
diagnostics, plain_put_url, SanitizedUrl{url, {}}, "PUT", azure_blob_headers(), data_filepath);
377+
INFO(diagnostics.to_string());
378+
CHECK(plain_put_success);
379+
}
380+
381+
{
382+
auto azcopy_put_filename = "azcopy_put_" + rnd;
383+
auto azcopy_put_url = url + azcopy_put_filename + query_string;
384+
url_pairs.emplace_back(azcopy_put_url, temp_dir / azcopy_put_filename);
385+
386+
FullyBufferedDiagnosticContext diagnostics{};
387+
auto azcopy_put_success =
388+
azcopy_to_asset_cache(diagnostics, azcopy_put_url, SanitizedUrl{url, {}}, data_filepath);
389+
INFO(diagnostics.to_string());
390+
CHECK(azcopy_put_success);
391+
}
392+
393+
{
394+
FullyBufferedDiagnosticContext diagnostics{};
395+
auto results = download_files_no_cache(diagnostics, url_pairs, azure_blob_headers(), {});
396+
INFO(diagnostics.to_string());
397+
CHECK(results == std::vector<int>{200, 200});
398+
}
399+
400+
for (auto& download : url_pairs)
401+
{
402+
auto download_filepath = download.second;
403+
CAPTURE(download_filepath);
404+
CHECK(fs.read_contents(download_filepath, VCPKG_LINE_INFO) == data);
405+
}
406+
407+
fs.remove_all(temp_dir, VCPKG_LINE_INFO);
408+
}

src/vcpkg/base/downloads.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,43 @@ namespace vcpkg
890890
return true;
891891
}
892892

893+
bool azcopy_to_asset_cache(DiagnosticContext& context,
894+
StringView raw_url,
895+
const SanitizedUrl& sanitized_url,
896+
const Path& file)
897+
{
898+
auto azcopy_cmd = Command{"azcopy"};
899+
azcopy_cmd.string_arg("copy");
900+
azcopy_cmd.string_arg("--from-to").string_arg("LocalBlob");
901+
azcopy_cmd.string_arg("--log-level").string_arg("NONE");
902+
azcopy_cmd.string_arg(file);
903+
azcopy_cmd.string_arg(raw_url.to_string());
904+
905+
int code = 0;
906+
auto res = cmd_execute_and_stream_lines(context, azcopy_cmd, [&code](StringView line) {
907+
static constexpr StringLiteral response_marker = "RESPONSE ";
908+
if (line.starts_with(response_marker))
909+
{
910+
code = std::strtol(line.data() + response_marker.size(), nullptr, 10);
911+
}
912+
});
913+
914+
auto pres = res.get();
915+
if (!pres)
916+
{
917+
return false;
918+
}
919+
920+
if (*pres != 0)
921+
{
922+
context.report_error(msg::format(
923+
msgAzcopyFailedToPutBlob, msg::exit_code = *pres, msg::url = sanitized_url, msg::value = code));
924+
return false;
925+
}
926+
927+
return true;
928+
}
929+
893930
std::string format_url_query(StringView base_url, View<std::string> query_params)
894931
{
895932
if (query_params.empty())

src/vcpkg/binarycaching.cpp

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,57 @@ namespace
506506
std::vector<std::string> m_secrets;
507507
};
508508

509+
struct AzureBlobPutBinaryProvider : IWriteBinaryProvider
510+
{
511+
AzureBlobPutBinaryProvider(const Filesystem& fs,
512+
std::vector<UrlTemplate>&& urls,
513+
const std::vector<std::string>& secrets)
514+
: m_fs(fs), m_urls(std::move(urls)), m_secrets(secrets)
515+
{
516+
}
517+
518+
size_t push_success(const BinaryPackageWriteInfo& request, MessageSink& msg_sink) override
519+
{
520+
if (!request.zip_path) return 0;
521+
522+
const auto& zip_path = *request.zip_path.get();
523+
524+
size_t count_stored = 0;
525+
const auto file_size = m_fs.file_size(zip_path, VCPKG_LINE_INFO);
526+
if (file_size == 0) return count_stored;
527+
528+
// cf.
529+
// https://learn.microsoft.com/en-us/rest/api/storageservices/understanding-block-blobs--append-blobs--and-page-blobs?toc=%2Fazure%2Fstorage%2Fblobs%2Ftoc.json
530+
constexpr size_t max_single_write = 5000000000;
531+
bool use_azcopy = file_size > max_single_write;
532+
533+
PrintingDiagnosticContext pdc{msg_sink};
534+
WarningDiagnosticContext wdc{pdc};
535+
536+
for (auto&& templ : m_urls)
537+
{
538+
auto url = templ.instantiate_variables(request);
539+
auto maybe_success =
540+
use_azcopy
541+
? azcopy_to_asset_cache(wdc, url, SanitizedUrl{url, m_secrets}, zip_path)
542+
: store_to_asset_cache(wdc, url, SanitizedUrl{url, m_secrets}, "PUT", templ.headers, zip_path);
543+
if (maybe_success)
544+
{
545+
count_stored++;
546+
}
547+
}
548+
return count_stored;
549+
}
550+
551+
bool needs_nuspec_data() const override { return false; }
552+
bool needs_zip_file() const override { return true; }
553+
554+
private:
555+
const Filesystem& m_fs;
556+
std::vector<UrlTemplate> m_urls;
557+
std::vector<std::string> m_secrets;
558+
};
559+
509560
struct NuGetSource
510561
{
511562
StringLiteral option;
@@ -1497,7 +1548,9 @@ namespace
14971548
segments[0].first);
14981549
}
14991550

1500-
if (!Strings::starts_with(segments[1].second, "https://"))
1551+
if (!Strings::starts_with(segments[1].second, "https://") &&
1552+
// Allow unencrypted Azurite for testing (not reflected in error msg)
1553+
!Strings::starts_with(segments[1].second, "http://127.0.0.1"))
15011554
{
15021555
return add_error(msg::format(msgInvalidArgumentRequiresBaseUrl,
15031556
msg::base_url = "https://",
@@ -1538,7 +1591,7 @@ namespace
15381591
if (read) state->url_templates_to_get.push_back(url_template);
15391592
auto headers = azure_blob_headers();
15401593
url_template.headers.assign(headers.begin(), headers.end());
1541-
if (write) state->url_templates_to_put.push_back(url_template);
1594+
if (write) state->azblob_templates_to_put.push_back(url_template);
15421595

15431596
state->binary_cache_providers.insert("azblob");
15441597
}
@@ -2287,6 +2340,11 @@ namespace vcpkg
22872340
m_config.write.push_back(
22882341
std::make_unique<FilesWriteBinaryProvider>(fs, std::move(s.archives_to_write)));
22892342
}
2343+
if (!s.azblob_templates_to_put.empty())
2344+
{
2345+
m_config.write.push_back(
2346+
std::make_unique<AzureBlobPutBinaryProvider>(fs, std::move(s.azblob_templates_to_put), s.secrets));
2347+
}
22902348
if (!s.url_templates_to_put.empty())
22912349
{
22922350
m_config.write.push_back(

0 commit comments

Comments
 (0)