diff options
author | Paul Cristian Sarbu <paul.cristian.sarbu@huawei.com> | 2023-12-13 10:08:59 +0100 |
---|---|---|
committer | Paul Cristian Sarbu <paul.cristian.sarbu@huawei.com> | 2023-12-14 17:08:51 +0100 |
commit | 2f7b219d01699d72d6d933366c10fd300a90f89d (patch) | |
tree | 7e691d71baaa7ddc629f77994d8978ef8baef451 | |
parent | 9cc4a6f8c66f207a78ed5df95db221a2bb3ed8f3 (diff) | |
download | justbuild-2f7b219d01699d72d6d933366c10fd300a90f89d.tar.gz |
just serve target description: Server-side implementation
-rw-r--r-- | src/buildtool/serve_api/serve_service/target.cpp | 171 | ||||
-rw-r--r-- | src/buildtool/serve_api/serve_service/target.hpp | 25 |
2 files changed, 196 insertions, 0 deletions
diff --git a/src/buildtool/serve_api/serve_service/target.cpp b/src/buildtool/serve_api/serve_service/target.cpp index ab4b24d2..c03e729b 100644 --- a/src/buildtool/serve_api/serve_service/target.cpp +++ b/src/buildtool/serve_api/serve_service/target.cpp @@ -620,3 +620,174 @@ auto TargetService::ServeTargetVariables( // respond with success return ::grpc::Status::OK; } + +auto TargetService::ServeTargetDescription( + ::grpc::ServerContext* /*context*/, + const ::justbuild::just_serve::ServeTargetDescriptionRequest* request, + ::justbuild::just_serve::ServeTargetDescriptionResponse* response) + -> ::grpc::Status { + auto const& root_tree{request->root_tree()}; + auto const& target_file{request->target_file()}; + auto const& target{request->target()}; + // retrieve content of target file + std::optional<std::string> target_file_content{std::nullopt}; + bool tree_found{false}; + // try in local build root Git cache + if (auto res = GetBlobContent( + StorageConfig::GitRoot(), root_tree, target_file, logger_)) { + tree_found = true; + if (res->first) { + if (not res->second) { + // tree exists, but does not contain target file + auto err = fmt::format( + "Target-root {} found, but does not contain target file {}", + root_tree, + target_file); + return ::grpc::Status{::grpc::StatusCode::FAILED_PRECONDITION, + err}; + } + target_file_content = *res->second; + } + } + if (not target_file_content) { + // try given extra repositories, in order + for (auto const& path : RemoteServeConfig::KnownRepositories()) { + if (auto res = + GetBlobContent(path, root_tree, target_file, logger_)) { + tree_found = true; + if (res->first) { + if (not res->second) { + // tree exists, but does not contain target file + auto err = fmt::format( + "Target-root {} found, but does not contain target " + "file {}", + root_tree, + target_file); + return ::grpc::Status{ + ::grpc::StatusCode::FAILED_PRECONDITION, err}; + } + target_file_content = *res->second; + break; + } + } + } + } + // report if failed to find root tree + if (not target_file_content) { + if (tree_found) { + // something went wrong trying to read the target file blob + auto err = + fmt::format("Could not read target file {}", target_file); + return ::grpc::Status{::grpc::StatusCode::INTERNAL, err}; + } + // tree not found + auto err = fmt::format("Missing target-root tree {}", root_tree); + return ::grpc::Status{::grpc::StatusCode::FAILED_PRECONDITION, err}; + } + // parse target file as json + ExpressionPtr map{nullptr}; + try { + map = Expression::FromJson(nlohmann::json::parse(*target_file_content)); + } catch (std::exception const& e) { + auto err = fmt::format( + "Failed to parse target file {} as json with error:\n{}", + target_file, + e.what()); + logger_->Emit(LogLevel::Error, err); + return ::grpc::Status{::grpc::StatusCode::FAILED_PRECONDITION, err}; + } + if (not map->IsMap()) { + auto err = + fmt::format("Target file {} should contain a map, but found:\n{}", + target_file, + map->ToString()); + logger_->Emit(LogLevel::Error, err); + return ::grpc::Status{::grpc::StatusCode::FAILED_PRECONDITION, err}; + } + // do validity checks (target is present and is of "type": "export") + auto target_desc = map->At(target); + if (not target_desc) { + // target is not present + auto err = fmt::format( + "Missing target {} in target file {}", target, target_file); + return ::grpc::Status{::grpc::StatusCode::FAILED_PRECONDITION, err}; + } + auto export_desc = target_desc->get()->At("type"); + if (not export_desc) { + auto err = fmt::format( + "Missing \"type\" field for target {} in target file {}.", + target, + target_file); + logger_->Emit(LogLevel::Error, err); + return ::grpc::Status{::grpc::StatusCode::FAILED_PRECONDITION, err}; + } + if (not export_desc->get()->IsString()) { + auto err = fmt::format( + "Field \"type\" for target {} in target file {} should be a " + "string, but found:\n{}", + target, + target_file, + export_desc->get()->ToString()); + logger_->Emit(LogLevel::Error, err); + return ::grpc::Status{::grpc::StatusCode::FAILED_PRECONDITION, err}; + } + if (export_desc->get()->String() != "export") { + // target is not of "type" : "export" + auto err = + fmt::format(R"(target {} is not of "type" : "export")", target); + return ::grpc::Status{::grpc::StatusCode::FAILED_PRECONDITION, err}; + } + // populate response description object with fields as-is + nlohmann::json desc{}; + if (auto doc = target_desc->get()->Get("doc", Expression::none_t{}); + doc.IsNotNull()) { + desc["doc"] = doc->ToJson(); + } + if (auto config_doc = + target_desc->get()->Get("config_doc", Expression::none_t{}); + config_doc.IsNotNull()) { + desc["config_doc"] = config_doc->ToJson(); + } + if (auto flexible_config = + target_desc->get()->Get("flexible_config", Expression::none_t{}); + flexible_config.IsNotNull()) { + desc["flexible_config"] = flexible_config->ToJson(); + } + + // acquire lock for CAS + auto lock = GarbageCollector::SharedLock(); + if (!lock) { + auto error_msg = fmt::format("Could not acquire gc SharedLock"); + logger_->Emit(LogLevel::Error, error_msg); + return ::grpc::Status{::grpc::StatusCode::INTERNAL, error_msg}; + } + + // store description blob to local CAS and sync with remote CAS; + // we keep the documentation strings as close to actual as possible, so we + // do not fail if they contain invalid UTF-8 characters, instead we use the + // UTF-8 replacement character to solve any decoding errors. + if (auto dgst = Storage::Instance().CAS().StoreBlob( + desc.dump(2, ' ', false, nlohmann::json::error_handler_t::replace), + /*is_executable=*/false)) { + auto const& artifact_dgst = ArtifactDigest{*dgst}; + if (not local_api_->RetrieveToCas( + {Artifact::ObjectInfo{.digest = artifact_dgst, + .type = ObjectType::File}}, + &*remote_api_)) { + auto error_msg = fmt::format( + "Failed to upload to remote cas the description blob {}", + artifact_dgst.hash()); + logger_->Emit(LogLevel::Error, error_msg); + return ::grpc::Status{::grpc::StatusCode::UNAVAILABLE, error_msg}; + } + + // populate response + *(response->mutable_description_id()) = *dgst; + return ::grpc::Status::OK; + } + // failed to store blob + const auto* const error_msg = + "Failed to store description blob to local cas"; + logger_->Emit(LogLevel::Error, error_msg); + return ::grpc::Status{::grpc::StatusCode::INTERNAL, error_msg}; +} diff --git a/src/buildtool/serve_api/serve_service/target.hpp b/src/buildtool/serve_api/serve_service/target.hpp index ef58e841..5f9e2d15 100644 --- a/src/buildtool/serve_api/serve_service/target.hpp +++ b/src/buildtool/serve_api/serve_service/target.hpp @@ -72,6 +72,31 @@ class TargetService final : public justbuild::just_serve::Target::Service { ::justbuild::just_serve::ServeTargetVariablesResponse* response) -> ::grpc::Status override; + // Given the target-level root tree and the name of an export target, + // returns the digest of the blob containing the flexible variables field, + // as well as the documentation fields pertaining tho the target and + // its configuration variables, as taken from the target's description. + // This information should be enough for a client to produce locally a + // full description of said target. + // + // The server MUST make the returned blob available in the remote CAS. + // + // If the status has a code different from `OK`, the response MUST not be + // used. + // + // Errors: + // * `FAILED_PRECONDITION`: An error occurred in retrieving the + // configuration of the requested target, such as missing entries + // (target-root, target file, target name), unparsable target file, or + // requested target not being of "type" : "export". + // * `UNAVAILABLE`: Could not communicate with the remote CAS. + // * `INTERNAL`: Internally, something is very broken. + auto ServeTargetDescription( + ::grpc::ServerContext* /*context*/, + const ::justbuild::just_serve::ServeTargetDescriptionRequest* request, + ::justbuild::just_serve::ServeTargetDescriptionResponse* response) + -> ::grpc::Status override; + private: std::shared_ptr<Logger> logger_{std::make_shared<Logger>("target-service")}; |