Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions base/cvd/cuttlefish/host/commands/cvd/cli/parser/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ cf_cc_library(
],
)

cf_cc_test(
name = "fetch_config_parser_test",
srcs = ["fetch_config_parser_test.cc"],
deps = [
"//cuttlefish/host/commands/cvd/cli/parser:cf_flags_validator",
"//cuttlefish/host/commands/cvd/cli/parser:fetch_config_parser",
"//cuttlefish/host/commands/cvd/cli/parser:load_config_cc_proto",
"//cuttlefish/host/commands/cvd/cli/parser:test_common",
"//cuttlefish/result",
"//cuttlefish/result:result_matchers",
"@jsoncpp",
],
)

cf_cc_test(
name = "flags_parser_test",
srcs = ["flags_parser_test.cc"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ bool ShouldFetch(const Instance& instance) {

for (const auto& value :
{disk.default_build(), disk.super_partition().system(),
boot.kernel().build(), boot.kernel().build(), boot.build(),
boot.bootloader().build(), disk.otatools()}) {
boot.kernel().build(), boot.build(), boot.bootloader().build(),
boot.android_efi_loader().build(), disk.otatools()}) {
// expects non-prefixed build strings already converted to empty strings
if (!value.empty()) {
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "cuttlefish/host/commands/cvd/cli/parser/fetch_config_parser.h"

#include <string>
#include <vector>

#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "json/value.h"

#include "cuttlefish/host/commands/cvd/cli/parser/cf_flags_validator.h"
#include "cuttlefish/host/commands/cvd/cli/parser/load_config.pb.h"
#include "cuttlefish/host/commands/cvd/cli/parser/test_common.h"
#include "cuttlefish/result/result.h"
#include "cuttlefish/result/result_matchers.h"

namespace cuttlefish {
namespace {

Result<std::vector<std::string>> FetchCvdParserTester(const Json::Value& root) {
const cvd::config::EnvironmentSpecification config =
CF_EXPECT(ValidateCfConfigs(root), "Json validation failed");
return ParseFetchCvdConfigs(config, "/tmp/fetch_test", {"0"});
}

} // namespace

TEST(FetchConfigParserTests, AndroidEfiLoaderBuildOnlySuccess) {
const char* test_string = R""""(
{
"instances": [
{
"boot": {
"android_efi_loader": {
"build": "@ab/branch/target"
}
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

Result<std::vector<std::string>> flags = FetchCvdParserTester(json_configs);
ASSERT_THAT(flags, IsOk());
EXPECT_TRUE(FindConfig(*flags, "--android_efi_loader_build=branch/target"));
}

TEST(FetchConfigParserTests, KernelBuildOnlySuccess) {
const char* test_string = R""""(
{
"instances": [
{
"boot": {
"kernel": {
"build": "@ab/branch/target"
}
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

Result<std::vector<std::string>> flags = FetchCvdParserTester(json_configs);
ASSERT_THAT(flags, IsOk());
EXPECT_TRUE(FindConfig(*flags, "--kernel_build=branch/target"));
}

TEST(FetchConfigParserTests, NoBuildStringsProducesNoFlagsSuccess) {
const char* test_string = R""""(
{
"instances": [
{
"vm": {
"memory_mb": 4096
}
}
]
}
)"""";

Json::Value json_configs;
std::string json_text(test_string);
ASSERT_TRUE(ParseJsonString(json_text, json_configs));

Result<std::vector<std::string>> flags = FetchCvdParserTester(json_configs);
ASSERT_THAT(flags, IsOk());
EXPECT_TRUE(flags->empty());
}

} // namespace cuttlefish
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ namespace cuttlefish {
namespace {

std::unique_ptr<CredentialSource> TryParseServiceAccount(
HttpClient& http_client, const std::string& file_content) {
HttpClient& http_client, const std::string& file_content,
const std::string& scope) {
Json::Reader reader;
Json::Value content;
if (!reader.parse(file_content, content)) {
Expand All @@ -45,8 +46,8 @@ std::unique_ptr<CredentialSource> TryParseServiceAccount(
VLOG(0) << "Could not parse credential file as Service Account";
return {};
}
auto result = ServiceAccountOauthCredentialSource::FromJson(
http_client, content, kAndroidBuildApiScope);
auto result = ServiceAccountOauthCredentialSource::FromJson(http_client,
content, scope);
if (!result.has_value()) {
VLOG(0) << "Failed to load service account json file: \n" << result.error();
return {};
Expand All @@ -56,7 +57,7 @@ std::unique_ptr<CredentialSource> TryParseServiceAccount(

Result<std::unique_ptr<CredentialSource>> GetCredentialSourceLegacy(
HttpClient& http_client, const std::string& credential_source,
const std::string& oauth_filepath) {
const std::string& oauth_filepath, const std::string& scope) {
std::unique_ptr<CredentialSource> result;
if (credential_source == "gce") {
result = GceMetadataCredentialSource::Make(http_client);
Expand Down Expand Up @@ -88,7 +89,7 @@ Result<std::unique_ptr<CredentialSource>> GetCredentialSourceLegacy(
CF_EXPECTF(ReadFileContents(credential_source),
"Failure getting credential file contents from file \"{}\"",
credential_source);
if (auto crds = TryParseServiceAccount(http_client, file_content)) {
if (auto crds = TryParseServiceAccount(http_client, file_content, scope)) {
result = std::move(crds);
} else {
result = FixedCredentialSource::Make(file_content);
Expand All @@ -101,7 +102,7 @@ Result<std::unique_ptr<CredentialSource>> GetCredentialSource(
HttpClient& http_client, const std::string& credential_source,
const std::string& oauth_filepath, const bool use_gce_metadata,
const std::string& credential_filepath,
const std::string& service_account_filepath) {
const std::string& service_account_filepath, const std::string& scope) {
const int number_of_set_credentials =
!credential_source.empty() + use_gce_metadata +
!credential_filepath.empty() + !service_account_filepath.empty();
Expand All @@ -125,7 +126,7 @@ Result<std::unique_ptr<CredentialSource>> GetCredentialSource(
"from file \"{}\".",
service_account_filepath);
auto service_account_credentials =
TryParseServiceAccount(http_client, contents);
TryParseServiceAccount(http_client, contents, scope);
CF_EXPECTF(service_account_credentials != nullptr,
"Unable to parse service account credentials in file \"{}\". "
"File contents: {}",
Expand All @@ -136,19 +137,19 @@ Result<std::unique_ptr<CredentialSource>> GetCredentialSource(
// when this helper is removed its `.acloud_oauth2.dat` processing should be
// moved here
return GetCredentialSourceLegacy(http_client, credential_source,
oauth_filepath);
oauth_filepath, scope);
}

} // namespace

Result<std::unique_ptr<CredentialSource>> GetCredentialSourceFromFlags(
HttpClient& http_client, const BuildApiFlags& flags,
const std::string& oauth_filepath) {
return CF_EXPECT(
GetCredentialSource(http_client, flags.credential_source, oauth_filepath,
flags.credential_flags.use_gce_metadata,
flags.credential_flags.credential_filepath,
flags.credential_flags.service_account_filepath));
const std::string& oauth_filepath, const std::string& scope) {
return CF_EXPECT(GetCredentialSource(
http_client, flags.credential_source, oauth_filepath,
flags.credential_flags.use_gce_metadata,
flags.credential_flags.credential_filepath,
flags.credential_flags.service_account_filepath, scope));
}

std::string GetAcloudOauthFilepath() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ namespace cuttlefish {
inline constexpr char kAndroidBuildApiScope[] =
"https://www.googleapis.com/auth/androidbuild.internal";

inline constexpr char kCloudStorageReadScope[] =
"https://www.googleapis.com/auth/devstorage.read_only";

Result<std::unique_ptr<CredentialSource>> GetCredentialSourceFromFlags(
HttpClient& http_client, const BuildApiFlags& flags,
const std::string& oauth_filepath);
const std::string& oauth_filepath,
const std::string& scope = kAndroidBuildApiScope);

std::string GetAcloudOauthFilepath();

Expand Down
6 changes: 3 additions & 3 deletions base/cvd/cuttlefish/host/commands/cvd/fetch/downloaders.cc
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ Result<Downloaders> Downloaders::Create(const BuildApiFlags& flags,
impl->luci_credential_source_ = CF_EXPECT(GetCredentialSourceFromFlags(
*impl->retrying_http_client_, flags,
StringFromEnv("HOME", ".") + "/.config/chrome_infra/auth/tokens.json"));
impl->gsutil_credential_source_ = CF_EXPECT(
GetCredentialSourceFromFlags(*impl->retrying_http_client_, flags,
StringFromEnv("HOME", ".") + "/.boto"));
impl->gsutil_credential_source_ = CF_EXPECT(GetCredentialSourceFromFlags(
*impl->retrying_http_client_, flags,
StringFromEnv("HOME", ".") + "/.boto", kCloudStorageReadScope));

impl->luci_build_api_ = std::make_unique<LuciBuildApi>(
*impl->retrying_http_client_, impl->luci_credential_source_.get(),
Expand Down
95 changes: 49 additions & 46 deletions base/cvd/cuttlefish/host/commands/cvd/fetch/fetch_cvd.cc
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Result<std::optional<Build>> GetBuildHelper(

Result<Builds> GetBuilds(BuildApi& build_api,
const BuildStrings& build_sources) {
Builds result = Builds{
return Builds{
.default_build = CF_EXPECT(GetBuildHelper(
build_api, build_sources.default_build, kDefaultBuildTarget)),
.system = CF_EXPECT(GetBuildHelper(build_api, build_sources.system_build,
Expand All @@ -146,14 +146,6 @@ Result<Builds> GetBuilds(BuildApi& build_api,
build_api, build_sources.test_suites_build, kDefaultBuildTarget)),
.chrome_os = build_sources.chrome_os_build,
};
if (!result.otatools) {
if (result.system) {
result.otatools = result.system;
} else if (result.kernel) {
result.otatools = result.default_build;
}
}
return {result};
}

Result<void> UpdateTargetsWithBuilds(BuildApi& build_api,
Expand Down Expand Up @@ -226,35 +218,39 @@ Result<void> FetchDefaultTarget(FetchBuildContext& context,
CF_EXPECT(img_zip.DeleteLocalFile());
}
}
std::string target_files_name = context.GetBuildZipName("target_files");
FetchArtifact target_files = context.Artifact(target_files_name);
if (has_system_build || flags.download_target_files_zip) {
LOG(INFO) << "Downloading target files zip for " << context;
std::string download_location =
fmt::format("default/{}", target_files_name);
CF_EXPECT(target_files.DownloadTo(download_location));
}
if (flags.dynamic_super_image) {
ReadableZip* target_files_zip = CF_EXPECT(target_files.AsZip());
std::unique_ptr<ReaderSeeker> ab_partitions_source =
CF_EXPECT(target_files_zip->OpenReadOnly("META/ab_partitions.txt"));
CF_EXPECT(ab_partitions_source.get());
std::string ab_partitions_contents =
CF_EXPECT(ReadToString(*ab_partitions_source));

CF_EXPECT(target_files.ExtractOneTo("META/ab_partitions.txt",
"default/ab_partitions.txt"));

std::vector<std::string_view> ab_files =
absl::StrSplit(ab_partitions_contents, '\n');
ab_files.emplace_back("super_empty");
for (std::string_view ab_file : ab_files) {
if (ab_file.empty()) {
continue;
const bool download_target_files =
has_system_build || flags.download_target_files_zip;
if (download_target_files || flags.dynamic_super_image) {
std::string target_files_name = context.GetBuildZipName("target_files");
FetchArtifact target_files = context.Artifact(target_files_name);
if (download_target_files) {
LOG(INFO) << "Downloading target files zip for " << context;
const std::string download_location =
fmt::format("default/{}", target_files_name);
CF_EXPECT(target_files.DownloadTo(download_location));
}
if (flags.dynamic_super_image) {
ReadableZip* target_files_zip = CF_EXPECT(target_files.AsZip());
std::unique_ptr<ReaderSeeker> ab_partitions_source =
CF_EXPECT(target_files_zip->OpenReadOnly("META/ab_partitions.txt"));
CF_EXPECT(ab_partitions_source.get());
const std::string ab_partitions_contents =
CF_EXPECT(ReadToString(*ab_partitions_source));

CF_EXPECT(target_files.ExtractOneTo("META/ab_partitions.txt",
"default/ab_partitions.txt"));

std::vector<std::string_view> ab_files =
absl::StrSplit(ab_partitions_contents, '\n');
ab_files.emplace_back("super_empty");
for (std::string_view ab_file : ab_files) {
if (ab_file.empty()) {
continue;
}
const std::string member = fmt::format("IMAGES/{}.img", ab_file);
const std::string output = fmt::format("default/{}.img", ab_file);
CF_EXPECT(target_files.ExtractOneTo(member, output));
}
std::string member = fmt::format("IMAGES/{}.img", ab_file);
std::string output = fmt::format("default/{}.img", ab_file);
CF_EXPECT(target_files.ExtractOneTo(member, output));
}
}
return {};
Expand Down Expand Up @@ -305,9 +301,11 @@ Result<void> FetchSystemTarget(FetchBuildContext& context,
}

Result<void> FetchKernelTarget(FetchBuildContext context) {
// If the kernel is from an arm/aarch64 build, the artifact will be called
// Image.
if (!context.Artifact("bzImage").DownloadTo("kernel").has_value()) {
if (std::optional<std::string> filepath = context.GetFilepath()) {
CF_EXPECT(context.Artifact(*filepath).DownloadTo("kernel"));
} else if (!context.Artifact("bzImage").DownloadTo("kernel").has_value()) {
// If the kernel is from an arm/aarch64 build, the artifact will be called
// Image.
CF_EXPECT(context.Artifact("Image").DownloadTo("kernel"));
}

Expand All @@ -320,12 +318,13 @@ Result<void> FetchKernelTarget(FetchBuildContext context) {

Result<void> FetchBootTarget(FetchBuildContext& context,
bool keep_downloaded_archives) {
std::string img_zip = context.GetBuildZipName("img");
std::string to_download = context.GetFilepath().value_or(img_zip);
const std::optional<std::string> filepath = context.GetFilepath();
const std::string to_download =
filepath.has_value() ? *filepath : context.GetBuildZipName("img");
FetchArtifact artifact = context.Artifact(to_download);
CF_EXPECT(artifact.Download());

if (to_download == img_zip) {
if (!filepath.has_value()) {
CF_EXPECT(artifact.ExtractOne("boot.img"));
CF_EXPECT(artifact.ExtractOne("vendor_boot.img"));
if (!keep_downloaded_archives) {
Expand All @@ -337,9 +336,13 @@ Result<void> FetchBootTarget(FetchBuildContext& context,
}

Result<void> FetchBootloaderTarget(FetchBuildContext& context) {
// If the bootloader is from an arm/aarch64 build, the artifact will be of
// filetype bin.
if (!context.Artifact("u-boot.rom").DownloadTo("bootloader").has_value()) {
if (std::optional<std::string> filepath = context.GetFilepath()) {
CF_EXPECT(context.Artifact(*filepath).DownloadTo("bootloader"));
} else if (!context.Artifact("u-boot.rom")
.DownloadTo("bootloader")
.has_value()) {
// If the bootloader is from an arm/aarch64 build, the artifact will be of
// filetype bin.
CF_EXPECT(context.Artifact("u-boot.bin").DownloadTo("bootloader"));
}
return {};
Expand Down
Loading
Loading