diff --git a/meson.build b/meson.build index 652b3e0..4cc9bd3 100644 --- a/meson.build +++ b/meson.build @@ -73,6 +73,7 @@ if uenv_cli uenv_src = [ 'src/cli/add_remove.cpp', 'src/cli/copy.cpp', + 'src/cli/delete.cpp', 'src/cli/find.cpp', 'src/cli/help.cpp', 'src/cli/image.cpp', diff --git a/src/cli/add_remove.cpp b/src/cli/add_remove.cpp index c979558..63f772b 100644 --- a/src/cli/add_remove.cpp +++ b/src/cli/add_remove.cpp @@ -67,9 +67,10 @@ int image_add(const image_add_args& args, const global_settings& settings) { label.error().message()); return 1; } - if (!label->partially_qualified()) { - term::error("the label {} does not provide at least name/version:tag", - args.uenv_description); + if (!label->fully_qualified()) { + term::error( + "the label {} must provide at name/version:tag@system%uarch", + args.uenv_description); return 1; } diff --git a/src/cli/delete.cpp b/src/cli/delete.cpp new file mode 100644 index 0000000..08a71e3 --- /dev/null +++ b/src/cli/delete.cpp @@ -0,0 +1,151 @@ +// vim: ts=4 sts=4 sw=4 et + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "delete.h" +#include "help.h" +#include "terminal.h" + +namespace uenv { + +std::string image_delete_footer(); + +void image_delete_args::add_cli(CLI::App& cli, + [[maybe_unused]] global_settings& settings) { + auto* delete_cli = + cli.add_subcommand("delete", "delete a uenv from a remote registry"); + delete_cli + ->add_option("uenv", uenv_description, + "either name/version:tag, sha256 or id") + ->required(); + delete_cli + ->add_option( + "--token", token, + "a path that contains a TOKEN file for accessing restricted uenv") + ->required(); + delete_cli->add_option("--username", username, + "user name for accessing restricted uenv."); + delete_cli->callback( + [&settings]() { settings.mode = uenv::cli_mode::image_delete; }); + + delete_cli->footer(image_delete_footer); +} + +int image_delete([[maybe_unused]] const image_delete_args& args, + [[maybe_unused]] const global_settings& settings) { + uenv::oras::credentials credentials; + if (auto c = site::get_credentials(args.username, args.token)) { + if (!*c) { + term::error("full credentials must be provided", c.error()); + } + credentials = (*c).value(); + } else { + term::error("{}", c.error()); + return 1; + } + spdlog::debug("registry credentials: {}", credentials); + + uenv_label label{}; + std::string nspace{}; + if (const auto parse = parse_uenv_nslabel(args.uenv_description)) { + label = parse->label; + if (!label.name || !parse->nspace) { + term::error("the uenv {} must provide at least a namespace and " + "name, e.g. 'build::f7076704830c8de7'", + args.uenv_description); + return 1; + } + nspace = parse->nspace.value(); + } else { + term::error("invalid uenv: {}", parse.error().message()); + return 1; + } + spdlog::debug("requested to delete {}::{}", nspace, label); + + auto registry = site::registry_listing(nspace); + if (!registry) { + term::error("unable to get a listing of the uenv", registry.error()); + return 1; + } + + // search db for matching records + const auto matches = registry->query(label); + if (!matches) { + term::error("invalid search term: {}", registry.error()); + return 1; + } + // check that there is one record with a unique sha + if (matches->empty()) { + using enum help::block::admonition; + term::error("no uenv found that matches '{}'\n\n{}", + args.uenv_description, + help::block(info, "try searching for the uenv to copy " + "first using 'uenv image find'")); + return 1; + } else if (!matches->unique_sha()) { + std::string errmsg = + fmt::format("more than one sha found that matches '{}':\n", + args.uenv_description); + errmsg += format_record_set(*matches); + term::error("{}", errmsg); + return 1; + } + + const auto rego_url = site::registry_url(); + spdlog::debug("registry url: {}", rego_url); + for (auto& record : *matches) { + // TODO: create a second pure JFrog API URL (now with added artifictory) + auto url = fmt::format( + "https://jfrog.svc.cscs.ch/artifactory/uenv/{}/{}/{}/{}/{}/{}", + nspace, record.system, record.uarch, record.name, record.version, + record.tag); + + if (auto result = + util::curl::del(url, credentials.username, credentials.token); + !result) { + term::error("unable to delete uenv: {}", result.error().message); + return 1; + } + + term::msg("delete {}", url); + } + + return 0; +} + +std::string image_delete_footer() { + using enum help::block::admonition; + std::vector items{ + // clang-format off + help::block{none, "Delete a uenv from a remote registry." }, + help::linebreak{}, + help::linebreak{}, + help::block{xmpl, "deploy a uenv from build to deploy namespace"}, + help::block{code, "uenv image delete build::prgenv-gnu/24.11:1551223269@todi%gh200"}, + help::block{code, "uenv image delete build::7890d67458ce7deb"}, + help::block{code, "uenv image delete deploy::prgenv-gnu/24.11:rc1@todi"}, + help::linebreak{}, + help::block{note, "the requested uenv must resolve to a unique sha."}, + // clang-format on + }; + + return fmt::format("{}", fmt::join(items, "\n")); +} + +} // namespace uenv diff --git a/src/cli/delete.h b/src/cli/delete.h new file mode 100644 index 0000000..c087362 --- /dev/null +++ b/src/cli/delete.h @@ -0,0 +1,41 @@ +#pragma once +// vim: ts=4 sts=4 sw=4 et + +#include +#include + +#include +#include + +#include + +#include "uenv.h" + +namespace uenv { + +struct image_delete_args { + std::string uenv_description; + std::optional token; + std::optional username; + void add_cli(CLI::App&, global_settings& settings); +}; + +int image_delete(const image_delete_args& args, + const global_settings& settings); + +} // namespace uenv + +template <> class fmt::formatter { + public: + // parse format specification and store it: + constexpr auto parse(format_parse_context& ctx) { + return ctx.end(); + } + // format a value using stored specification: + template + constexpr auto format(uenv::image_delete_args const& opts, + FmtContext& ctx) const { + return fmt::format_to(ctx.out(), "(image delete {} .token={})", + opts.uenv_description, opts.token); + } +}; diff --git a/src/cli/image.cpp b/src/cli/image.cpp index a69a5d7..2de4c90 100644 --- a/src/cli/image.cpp +++ b/src/cli/image.cpp @@ -14,6 +14,7 @@ #include "add_remove.h" #include "copy.h" +#include "delete.h" #include "help.h" #include "image.h" #include "inspect.h" @@ -34,6 +35,9 @@ void image_args::add_cli(CLI::App& cli, // add the `uenv image copy` command copy_args.add_cli(*image_cli, settings); + // add the `uenv image delete` command + delete_args.add_cli(*image_cli, settings); + // add the `uenv image add` command add_args.add_cli(*image_cli, settings); diff --git a/src/cli/image.h b/src/cli/image.h index 97c0ba9..3dca305 100644 --- a/src/cli/image.h +++ b/src/cli/image.h @@ -6,6 +6,7 @@ #include "add_remove.h" #include "copy.h" +#include "delete.h" #include "find.h" #include "inspect.h" #include "ls.h" @@ -19,6 +20,7 @@ void image_help(); struct image_args { image_add_args add_args; image_copy_args copy_args; + image_delete_args delete_args; image_find_args find_args; image_inspect_args inspect_args; image_ls_args ls_args; diff --git a/src/cli/uenv.cpp b/src/cli/uenv.cpp index 8291628..1f3dd3e 100644 --- a/src/cli/uenv.cpp +++ b/src/cli/uenv.cpp @@ -15,6 +15,7 @@ #include "add_remove.h" #include "build.h" +#include "delete.h" #include "help.h" #include "image.h" #include "repo.h" @@ -130,6 +131,8 @@ int main(int argc, char** argv) { return uenv::image_add(image.add_args, settings); case settings.image_copy: return uenv::image_copy(image.copy_args, settings); + case settings.image_delete: + return uenv::image_delete(image.delete_args, settings); case settings.image_inspect: return uenv::image_inspect(image.inspect_args, settings); case settings.image_rm: diff --git a/src/cli/uenv.h b/src/cli/uenv.h index f4ac92f..ba3cd59 100644 --- a/src/cli/uenv.h +++ b/src/cli/uenv.h @@ -16,6 +16,7 @@ enum class cli_mode : std::uint32_t { unset, image_add, image_copy, + image_delete, image_find, image_inspect, image_ls, @@ -68,6 +69,8 @@ template <> class fmt::formatter { return format_to(ctx.out(), "image-add"); case image_copy: return format_to(ctx.out(), "image-copy"); + case image_delete: + return format_to(ctx.out(), "image-delete"); case image_rm: return format_to(ctx.out(), "image-rm"); case image_find: diff --git a/src/uenv/oras.cpp b/src/uenv/oras.cpp index fae9d41..163b466 100644 --- a/src/uenv/oras.cpp +++ b/src/uenv/oras.cpp @@ -196,7 +196,7 @@ util::expected pull_tag(const std::string& registry, }); while (!proc->finished()) { std::this_thread::sleep_for(100ms); - // handle a signacl, usually SIGTERM or SIGINT + // handle a signal, usually SIGTERM or SIGINT if (util::signal_raised()) { spdlog::warn("signal raised - interrupting download"); throw util::signal_exception(util::last_signal_raised()); @@ -237,7 +237,6 @@ copy(const std::string& registry, const std::string& src_nspace, args.push_back(fmt::format("--to-password={}", token->token)); args.push_back(fmt::format("--to-username={}", token->username)); } - // fmt::println("oras {}", fmt::join(args, " ")); auto result = run_oras(args); if (result.rcode) { diff --git a/src/util/curl.cpp b/src/util/curl.cpp index 7f2de3b..637fc5f 100644 --- a/src/util/curl.cpp +++ b/src/util/curl.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -10,12 +11,26 @@ #include #include -#include - namespace util { namespace curl { +std::string http_message(long code) { + const char* default_message = + "internal error contacting a network service - please create a CSCS " + "service desk request with the output of running this command with the " + "-vvv flag"; + const static std::unordered_map messages = { + {403, "the provided credentials were invalid - you might not have " + "permission to access the requested resource."}, + {408, + "there was a time out contacting an external service - please retry " + "later and create a CSCS Service Desk issue if the issue persists"}, + }; + + return messages.count(code) ? messages.at(code) : default_message; +} + #define CURL_EASY(CMD) \ if (auto rval__ = CMD; rval__ != CURLE_OK) { \ return util::unexpected(error{rval__, errbuf}); \ @@ -36,7 +51,6 @@ expected get(std::string url) { char errbuf[CURL_ERROR_SIZE]; errbuf[0] = 0; - // auto h = curl::make_handle(); auto h = curl_easy_init(); if (!h) { return unexpected{ @@ -138,12 +152,11 @@ expected upload(std::string url, CURL_EASY(curl_easy_setopt(h, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1)); - // Store HTTP status code - long http_code = 0; - // Perform the request CURL_EASY(curl_easy_perform(h)); + // Get the HTTP response code + long http_code = 0; CURL_EASY(curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &http_code)); spdlog::trace("curl::upload http_code: {}", http_code); @@ -153,12 +166,82 @@ expected upload(std::string url, // store stdout std::string curl_stdout{result.data(), result.data() + result.size()}; - if (http_code != 200) { - return unexpected{error{CURLE_OK, curl_stdout}}; + if (http_code >= 400) { + return unexpected{ + error{CURLE_HTTP_RETURNED_ERROR, + fmt::format("{}: {}", http_code, http_message(http_code))}}; } return curl_stdout; } +expected del(std::string url, std::string username, + std::string token) { + char errbuf[CURL_ERROR_SIZE]; + errbuf[0] = 0; + + auto h = curl_easy_init(); + if (!h) { + return unexpected{ + error{CURLE_FAILED_INIT, "unable to initialise curl"}}; + } + auto _ = defer([h]() { curl_easy_cleanup(h); }); + + CURL_EASY(curl_easy_setopt(h, CURLOPT_ERRORBUFFER, errbuf)); + + CURL_EASY(curl_easy_setopt(h, CURLOPT_URL, url.c_str())); + spdlog::trace("curl::get set url {}", url); + + // send all data to this function + CURL_EASY(curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, memory_callback)); + spdlog::trace("curl::get set memory callback"); + + // we pass our 'chunk' struct to the callback function + std::vector result; + result.reserve(10000); + CURL_EASY(curl_easy_setopt(h, CURLOPT_WRITEDATA, (void*)&result)); + spdlog::trace("curl::get set memory target"); + + // some servers do not like requests that are made without a user-agent + // field, so we provide one + CURL_EASY(curl_easy_setopt(h, CURLOPT_USERAGENT, "libcurl-agent/1.0")); + spdlog::trace("curl::get set user agent"); + + // Specify the HTTP method as DELETE + CURL_EASY(curl_easy_setopt(h, CURLOPT_CUSTOMREQUEST, "DELETE")); + + // Set the username and password for basic authentication + CURL_EASY(curl_easy_setopt(h, CURLOPT_USERNAME, username.c_str())); + CURL_EASY(curl_easy_setopt(h, CURLOPT_PASSWORD, token.c_str())); + spdlog::trace("curl::get set credentials"); + + CURL_EASY(curl_easy_setopt(h, CURLOPT_CONNECTTIMEOUT_MS, 1000L)); + CURL_EASY(curl_easy_setopt(h, CURLOPT_TIMEOUT_MS, 10000L)); + spdlog::trace("curl::get set timeout"); + + CURL_EASY(curl_easy_perform(h)); + + // Get the HTTP response code + long http_code = 0; + CURL_EASY(curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &http_code)); + spdlog::trace("curl::upload http_code: {}", http_code); + + // store stdout + std::string curl_stdout{result.data(), result.data() + result.size()}; + + if (http_code >= 400) { + return unexpected{ + error{CURLE_HTTP_RETURNED_ERROR, + fmt::format("{}: {}", http_code, http_message(http_code))}}; + } + + spdlog::info("curl -X DELETE -u {}:{} {}", username, + std::string(token.size(), 'X'), url); + + spdlog::trace("curl::get successfully deleted {}", url); + + return {}; +} + } // namespace curl } // namespace util diff --git a/src/util/curl.h b/src/util/curl.h index 4c520ea..697abc5 100644 --- a/src/util/curl.h +++ b/src/util/curl.h @@ -22,5 +22,8 @@ expected get(std::string url); expected upload(std::string url, std::filesystem::path file_name); +expected del(std::string url, std::string username, + std::string password); + } // namespace curl } // namespace util