1 //
2 // Copyright (C) 2019 The Android Open Source Project
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15
16 #include "build_api.h"
17
18 #include <dirent.h>
19 #include <unistd.h>
20
21 #include <chrono>
22 #include <memory>
23 #include <set>
24 #include <string>
25 #include <thread>
26 #include <tuple>
27 #include <utility>
28 #include <vector>
29
30 #include <android-base/logging.h>
31 #include <android-base/strings.h>
32
33 #include "common/libs/utils/environment.h"
34 #include "common/libs/utils/files.h"
35 #include "common/libs/utils/result.h"
36 #include "host/libs/web/credential_source.h"
37
38 namespace cuttlefish {
39 namespace {
40
41 const std::string BUILD_API =
42 "https://www.googleapis.com/android/internal/build/v3";
43
StatusIsTerminal(const std::string & status)44 bool StatusIsTerminal(const std::string& status) {
45 const static std::set<std::string> terminal_statuses = {
46 "abandoned", "complete", "error", "ABANDONED", "COMPLETE", "ERROR",
47 };
48 return terminal_statuses.count(status) > 0;
49 }
50
ArtifactsContain(const std::vector<Artifact> & artifacts,const std::string & name)51 bool ArtifactsContain(const std::vector<Artifact>& artifacts,
52 const std::string& name) {
53 for (const auto& artifact : artifacts) {
54 if (artifact.Name() == name) {
55 return true;
56 }
57 }
58 return false;
59 }
60
BuildNameRegexp(const std::vector<std::string> & artifact_filenames)61 std::string BuildNameRegexp(
62 const std::vector<std::string>& artifact_filenames) {
63 // surrounding with \Q and \E treats the text literally to avoid
64 // characters being treated as regex
65 auto it = artifact_filenames.begin();
66 std::string name_regex = "^\\Q" + *it + "\\E$";
67 std::string result = name_regex;
68 ++it;
69 for (const auto end = artifact_filenames.end(); it != end; ++it) {
70 name_regex = "^\\Q" + *it + "\\E$";
71 result += "|" + name_regex;
72 }
73 return result;
74 }
75
76 } // namespace
77
Artifact(const Json::Value & json_artifact)78 Artifact::Artifact(const Json::Value& json_artifact) {
79 name_ = json_artifact["name"].asString();
80 size_ = std::stol(json_artifact["size"].asString());
81 last_modified_time_ = std::stol(json_artifact["lastModifiedTime"].asString());
82 md5_ = json_artifact["md5"].asString();
83 content_type_ = json_artifact["contentType"].asString();
84 revision_ = json_artifact["revision"].asString();
85 creation_time_ = std::stol(json_artifact["creationTime"].asString());
86 crc32_ = json_artifact["crc32"].asUInt();
87 }
88
operator <<(std::ostream & out,const DeviceBuild & build)89 std::ostream& operator<<(std::ostream& out, const DeviceBuild& build) {
90 return out << "(id=\"" << build.id << "\", target=\"" << build.target
91 << "\")";
92 }
93
operator <<(std::ostream & out,const DirectoryBuild & build)94 std::ostream& operator<<(std::ostream& out, const DirectoryBuild& build) {
95 auto paths = android::base::Join(build.paths, ":");
96 return out << "(paths=\"" << paths << "\", target=\"" << build.target
97 << "\")";
98 }
99
operator <<(std::ostream & out,const Build & build)100 std::ostream& operator<<(std::ostream& out, const Build& build) {
101 std::visit([&out](auto&& arg) { out << arg; }, build);
102 return out;
103 }
104
DirectoryBuild(std::vector<std::string> paths,std::string target)105 DirectoryBuild::DirectoryBuild(std::vector<std::string> paths,
106 std::string target)
107 : paths(std::move(paths)), target(std::move(target)), id("eng") {
108 product = StringFromEnv("TARGET_PRODUCT", "");
109 }
110
BuildApi()111 BuildApi::BuildApi() : BuildApi(std::move(HttpClient::CurlClient()), nullptr) {}
112
BuildApi(std::unique_ptr<HttpClient> http_client,std::unique_ptr<CredentialSource> credential_source)113 BuildApi::BuildApi(std::unique_ptr<HttpClient> http_client,
114 std::unique_ptr<CredentialSource> credential_source)
115 : BuildApi(std::move(http_client), nullptr, std::move(credential_source),
116 "", std::chrono::seconds(0)) {}
117
BuildApi(std::unique_ptr<HttpClient> http_client,std::unique_ptr<HttpClient> inner_http_client,std::unique_ptr<CredentialSource> credential_source,std::string api_key,const std::chrono::seconds retry_period)118 BuildApi::BuildApi(std::unique_ptr<HttpClient> http_client,
119 std::unique_ptr<HttpClient> inner_http_client,
120 std::unique_ptr<CredentialSource> credential_source,
121 std::string api_key, const std::chrono::seconds retry_period)
122 : http_client(std::move(http_client)),
123 inner_http_client(std::move(inner_http_client)),
124 credential_source(std::move(credential_source)),
125 api_key_(std::move(api_key)),
126 retry_period_(retry_period) {}
127
Headers()128 Result<std::vector<std::string>> BuildApi::Headers() {
129 std::vector<std::string> headers;
130 if (credential_source) {
131 headers.push_back("Authorization: Bearer " +
132 CF_EXPECT(credential_source->Credential()));
133 }
134 return headers;
135 }
136
LatestBuildId(const std::string & branch,const std::string & target)137 Result<std::string> BuildApi::LatestBuildId(const std::string& branch,
138 const std::string& target) {
139 std::string url =
140 BUILD_API + "/builds?branch=" + http_client->UrlEscape(branch) +
141 "&buildAttemptStatus=complete" +
142 "&buildType=submitted&maxResults=1&successful=true&target=" +
143 http_client->UrlEscape(target);
144 if (!api_key_.empty()) {
145 url += "&key=" + http_client->UrlEscape(api_key_);
146 }
147 auto response =
148 CF_EXPECT(http_client->DownloadToJson(url, CF_EXPECT(Headers())));
149 const auto& json = response.data;
150 CF_EXPECT(response.HttpSuccess(), "Error fetching the latest build of \""
151 << target << "\" on \"" << branch
152 << "\". The server response was \""
153 << json << "\", and code was "
154 << response.http_code);
155 CF_EXPECT(!json.isMember("error"),
156 "Response had \"error\" but had http success status. Received \""
157 << json << "\"");
158
159 if (!json.isMember("builds") || json["builds"].size() != 1) {
160 LOG(WARNING) << "expected to receive 1 build for \"" << target << "\" on \""
161 << branch << "\", but received " << json["builds"].size()
162 << ". Full response was " << json;
163 // TODO(schuffelen): Return a failed Result here, and update ArgumentToBuild
164 return "";
165 }
166 return json["builds"][0]["buildId"].asString();
167 }
168
BuildStatus(const DeviceBuild & build)169 Result<std::string> BuildApi::BuildStatus(const DeviceBuild& build) {
170 std::string url = BUILD_API + "/builds/" + http_client->UrlEscape(build.id) +
171 "/" + http_client->UrlEscape(build.target);
172 if (!api_key_.empty()) {
173 url += "?key=" + http_client->UrlEscape(api_key_);
174 }
175 auto response =
176 CF_EXPECT(http_client->DownloadToJson(url, CF_EXPECT(Headers())));
177 const auto& json = response.data;
178 CF_EXPECT(response.HttpSuccess(),
179 "Error fetching the status of \""
180 << build << "\". The server response was \"" << json
181 << "\", and code was " << response.http_code);
182 CF_EXPECT(!json.isMember("error"),
183 "Response had \"error\" but had http success status. Received \""
184 << json << "\"");
185
186 return json["buildAttemptStatus"].asString();
187 }
188
ProductName(const DeviceBuild & build)189 Result<std::string> BuildApi::ProductName(const DeviceBuild& build) {
190 std::string url = BUILD_API + "/builds/" + http_client->UrlEscape(build.id) +
191 "/" + http_client->UrlEscape(build.target);
192 if (!api_key_.empty()) {
193 url += "?key=" + http_client->UrlEscape(api_key_);
194 }
195 auto response =
196 CF_EXPECT(http_client->DownloadToJson(url, CF_EXPECT(Headers())));
197 const auto& json = response.data;
198 CF_EXPECT(response.HttpSuccess(),
199 "Error fetching the product name of \""
200 << build << "\". The server response was \"" << json
201 << "\", and code was " << response.http_code);
202 CF_EXPECT(!json.isMember("error"),
203 "Response had \"error\" but had http success status. Received \""
204 << json << "\"");
205
206 CF_EXPECT(json.isMember("target"), "Build was missing target field.");
207 return json["target"]["product"].asString();
208 }
209
Artifacts(const DeviceBuild & build,const std::vector<std::string> & artifact_filenames)210 Result<std::vector<Artifact>> BuildApi::Artifacts(
211 const DeviceBuild& build,
212 const std::vector<std::string>& artifact_filenames) {
213 std::string page_token = "";
214 std::vector<Artifact> artifacts;
215 do {
216 std::string url = BUILD_API + "/builds/" +
217 http_client->UrlEscape(build.id) + "/" +
218 http_client->UrlEscape(build.target) +
219 "/attempts/latest/artifacts?maxResults=100";
220 if (!artifact_filenames.empty()) {
221 url += "&nameRegexp=" +
222 http_client->UrlEscape(BuildNameRegexp(artifact_filenames));
223 }
224 if (page_token != "") {
225 url += "&pageToken=" + http_client->UrlEscape(page_token);
226 }
227 if (!api_key_.empty()) {
228 url += "&key=" + http_client->UrlEscape(api_key_);
229 }
230 auto response =
231 CF_EXPECT(http_client->DownloadToJson(url, CF_EXPECT(Headers())));
232 const auto& json = response.data;
233 CF_EXPECT(response.HttpSuccess(),
234 "Error fetching the artifacts of \""
235 << build << "\". The server response was \"" << json
236 << "\", and code was " << response.http_code);
237 CF_EXPECT(!json.isMember("error"),
238 "Response had \"error\" but had http success status. Received \""
239 << json << "\"");
240 if (json.isMember("nextPageToken")) {
241 page_token = json["nextPageToken"].asString();
242 } else {
243 page_token = "";
244 }
245 for (const auto& artifact_json : json["artifacts"]) {
246 artifacts.emplace_back(artifact_json);
247 }
248 } while (page_token != "");
249 return artifacts;
250 }
251
252 struct CloseDir {
operator ()cuttlefish::CloseDir253 void operator()(DIR* dir) { closedir(dir); }
254 };
255
Artifacts(const DirectoryBuild & build,const std::vector<std::string> &)256 Result<std::vector<Artifact>> BuildApi::Artifacts(
257 const DirectoryBuild& build, const std::vector<std::string>&) {
258 std::vector<Artifact> artifacts;
259 for (const auto& path : build.paths) {
260 auto dir = std::unique_ptr<DIR, CloseDir>(opendir(path.c_str()));
261 CF_EXPECT(dir != nullptr, "Could not read files from \"" << path << "\"");
262 for (auto entity = readdir(dir.get()); entity != nullptr;
263 entity = readdir(dir.get())) {
264 artifacts.emplace_back(std::string(entity->d_name));
265 }
266 }
267 return artifacts;
268 }
269
ArtifactToCallback(const DeviceBuild & build,const std::string & artifact,HttpClient::DataCallback callback)270 Result<void> BuildApi::ArtifactToCallback(const DeviceBuild& build,
271 const std::string& artifact,
272 HttpClient::DataCallback callback) {
273 std::string download_url_endpoint =
274 BUILD_API + "/builds/" + http_client->UrlEscape(build.id) + "/" +
275 http_client->UrlEscape(build.target) + "/attempts/latest/artifacts/" +
276 http_client->UrlEscape(artifact) + "/url";
277 if (!api_key_.empty()) {
278 download_url_endpoint += "?key=" + http_client->UrlEscape(api_key_);
279 }
280 auto response = CF_EXPECT(
281 http_client->DownloadToJson(download_url_endpoint, CF_EXPECT(Headers())));
282 const auto& json = response.data;
283 CF_EXPECT(response.HttpSuccess() || response.HttpRedirect(),
284 "Error fetching the url of \"" << artifact << "\" for \"" << build
285 << "\". The server response was \""
286 << json << "\", and code was "
287 << response.http_code);
288 CF_EXPECT(!json.isMember("error"),
289 "Response had \"error\" but had http success status. "
290 << "Received \"" << json << "\"");
291 CF_EXPECT(json.isMember("signedUrl"),
292 "URL endpoint did not have json path: " << json);
293 std::string url = json["signedUrl"].asString();
294 auto callback_response =
295 CF_EXPECT(http_client->DownloadToCallback(callback, url));
296 CF_EXPECT(IsHttpSuccess(callback_response.http_code));
297 return {};
298 }
299
ArtifactToFile(const DeviceBuild & build,const std::string & artifact,const std::string & path)300 Result<void> BuildApi::ArtifactToFile(const DeviceBuild& build,
301 const std::string& artifact,
302 const std::string& path) {
303 std::string download_url_endpoint =
304 BUILD_API + "/builds/" + http_client->UrlEscape(build.id) + "/" +
305 http_client->UrlEscape(build.target) + "/attempts/latest/artifacts/" +
306 http_client->UrlEscape(artifact) + "/url";
307 if (!api_key_.empty()) {
308 download_url_endpoint += "?key=" + http_client->UrlEscape(api_key_);
309 }
310 auto response = CF_EXPECT(
311 http_client->DownloadToJson(download_url_endpoint, CF_EXPECT(Headers())));
312 const auto& json = response.data;
313 CF_EXPECT(response.HttpSuccess() || response.HttpRedirect(),
314 "Error fetching the url of \"" << artifact << "\" for \"" << build
315 << "\". The server response was \""
316 << json << "\", and code was "
317 << response.http_code);
318 CF_EXPECT(!json.isMember("error"),
319 "Response had \"error\" but had http success status. "
320 << "Received \"" << json << "\"");
321 CF_EXPECT(json.isMember("signedUrl"),
322 "URL endpoint did not have json path: " << json);
323 std::string url = json["signedUrl"].asString();
324 CF_EXPECT(CF_EXPECT(http_client->DownloadToFile(url, path)).HttpSuccess());
325 return {};
326 }
327
ArtifactToFile(const DirectoryBuild & build,const std::string & artifact,const std::string & destination)328 Result<void> BuildApi::ArtifactToFile(const DirectoryBuild& build,
329 const std::string& artifact,
330 const std::string& destination) {
331 for (const auto& path : build.paths) {
332 auto source = path + "/" + artifact;
333 if (!FileExists(source)) {
334 continue;
335 }
336 unlink(destination.c_str());
337 CF_EXPECT(symlink(source.c_str(), destination.c_str()) == 0,
338 "Could not create symlink from " << source << " to "
339 << destination << ": "
340 << strerror(errno));
341 return {};
342 }
343 return CF_ERR("Could not find artifact \"" << artifact << "\" in build \""
344 << build << "\"");
345 }
346
ArgumentToBuild(const std::string & arg,const std::string & default_build_target)347 Result<Build> BuildApi::ArgumentToBuild(
348 const std::string& arg, const std::string& default_build_target) {
349 if (arg.find(':') != std::string::npos) {
350 std::vector<std::string> dirs = android::base::Split(arg, ":");
351 std::string id = dirs.back();
352 dirs.pop_back();
353 return DirectoryBuild(dirs, id);
354 }
355 size_t slash_pos = arg.find('/');
356 if (slash_pos != std::string::npos &&
357 arg.find('/', slash_pos + 1) != std::string::npos) {
358 return CF_ERR("Build argument cannot have more than one '/' slash. Was at "
359 << slash_pos << " and " << arg.find('/', slash_pos + 1));
360 }
361 std::string build_target = slash_pos == std::string::npos
362 ? default_build_target
363 : arg.substr(slash_pos + 1);
364 std::string branch_or_id =
365 slash_pos == std::string::npos ? arg : arg.substr(0, slash_pos);
366 std::string branch_latest_build_id =
367 CF_EXPECT(LatestBuildId(branch_or_id, build_target));
368 std::string build_id = branch_or_id;
369 if (branch_latest_build_id != "") {
370 LOG(INFO) << "The latest good build on branch \"" << branch_or_id
371 << "\"with build target \"" << build_target << "\" is \""
372 << branch_latest_build_id << "\"";
373 build_id = branch_latest_build_id;
374 }
375 DeviceBuild proposed_build = DeviceBuild(build_id, build_target);
376 std::string status = CF_EXPECT(BuildStatus(proposed_build));
377 CF_EXPECT(status != "",
378 proposed_build << " is not a valid branch or build id.");
379 LOG(INFO) << "Status for build " << proposed_build << " is " << status;
380 while (retry_period_ != std::chrono::seconds::zero() &&
381 !StatusIsTerminal(status)) {
382 LOG(INFO) << "Status is \"" << status << "\". Waiting for "
383 << retry_period_.count() << " seconds.";
384 std::this_thread::sleep_for(retry_period_);
385 status = CF_EXPECT(BuildStatus(proposed_build));
386 }
387 LOG(INFO) << "Status for build " << proposed_build << " is " << status;
388 proposed_build.product = CF_EXPECT(ProductName(proposed_build));
389 return proposed_build;
390 }
391
DownloadFile(const Build & build,const std::string & target_directory,const std::string & artifact_name)392 Result<std::string> BuildApi::DownloadFile(const Build& build,
393 const std::string& target_directory,
394 const std::string& artifact_name) {
395 std::vector<Artifact> artifacts =
396 CF_EXPECT(Artifacts(build, {artifact_name}));
397 CF_EXPECT(ArtifactsContain(artifacts, artifact_name),
398 "Target " << build << " did not contain " << artifact_name);
399 return DownloadTargetFile(build, target_directory, artifact_name);
400 }
401
DownloadFileWithBackup(const Build & build,const std::string & target_directory,const std::string & artifact_name,const std::string & backup_artifact_name)402 Result<std::string> BuildApi::DownloadFileWithBackup(
403 const Build& build, const std::string& target_directory,
404 const std::string& artifact_name, const std::string& backup_artifact_name) {
405 std::vector<Artifact> artifacts =
406 CF_EXPECT(Artifacts(build, {artifact_name, backup_artifact_name}));
407 std::string selected_artifact = artifact_name;
408 if (!ArtifactsContain(artifacts, artifact_name)) {
409 selected_artifact = backup_artifact_name;
410 }
411 return DownloadTargetFile(build, target_directory, selected_artifact);
412 }
413
DownloadTargetFile(const Build & build,const std::string & target_directory,const std::string & artifact_name)414 Result<std::string> BuildApi::DownloadTargetFile(
415 const Build& build, const std::string& target_directory,
416 const std::string& artifact_name) {
417 std::string target_filepath = target_directory + "/" + artifact_name;
418 CF_EXPECT(ArtifactToFile(build, artifact_name, target_filepath),
419 "Unable to download " << build << ":" << artifact_name << " to "
420 << target_filepath);
421 return {target_filepath};
422 }
423
424 /** Returns the name of one of the artifact target zip files.
425 *
426 * For example, for a target "aosp_cf_x86_phone-userdebug" at a build "5824130",
427 * the image zip file would be "aosp_cf_x86_phone-img-5824130.zip"
428 */
GetBuildZipName(const Build & build,const std::string & name)429 std::string GetBuildZipName(const Build& build, const std::string& name) {
430 std::string product =
431 std::visit([](auto&& arg) { return arg.product; }, build);
432 auto id = std::visit([](auto&& arg) { return arg.id; }, build);
433 return product + "-" + name + "-" + id + ".zip";
434 }
435
436 } // namespace cuttlefish
437