• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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