• 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 <set>
23 #include <string>
24 #include <thread>
25 
26 #include <android-base/strings.h>
27 #include <android-base/logging.h>
28 
29 #include "common/libs/utils/environment.h"
30 #include "common/libs/utils/files.h"
31 
32 namespace cuttlefish {
33 namespace {
34 
35 const std::string BUILD_API =
36     "https://www.googleapis.com/android/internal/build/v3";
37 
StatusIsTerminal(const std::string & status)38 bool StatusIsTerminal(const std::string& status) {
39   const static std::set<std::string> terminal_statuses = {
40     "abandoned",
41     "complete",
42     "error",
43     "ABANDONED",
44     "COMPLETE",
45     "ERROR",
46   };
47   return terminal_statuses.count(status) > 0;
48 }
49 
50 } // namespace
51 
Artifact(const Json::Value & json_artifact)52 Artifact::Artifact(const Json::Value& json_artifact) {
53   name = json_artifact["name"].asString();
54   size = std::stol(json_artifact["size"].asString());
55   last_modified_time = std::stol(json_artifact["lastModifiedTime"].asString());
56   md5 = json_artifact["md5"].asString();
57   content_type = json_artifact["contentType"].asString();
58   revision = json_artifact["revision"].asString();
59   creation_time = std::stol(json_artifact["creationTime"].asString());
60   crc32 = json_artifact["crc32"].asUInt();
61 }
62 
operator <<(std::ostream & out,const DeviceBuild & build)63 std::ostream& operator<<(std::ostream& out, const DeviceBuild& build) {
64   return out << "(id=\"" << build.id << "\", target=\"" << build.target << "\")";
65 }
66 
operator <<(std::ostream & out,const DirectoryBuild & build)67 std::ostream& operator<<(std::ostream& out, const DirectoryBuild& build) {
68   auto paths = android::base::Join(build.paths, ":");
69   return out << "(paths=\"" << paths << "\", target=\"" << build.target << "\")";
70 }
71 
operator <<(std::ostream & out,const Build & build)72 std::ostream& operator<<(std::ostream& out, const Build& build) {
73   std::visit([&out](auto&& arg) { out << arg; }, build);
74   return out;
75 }
76 
DirectoryBuild(const std::vector<std::string> & paths,const std::string & target)77 DirectoryBuild::DirectoryBuild(const std::vector<std::string>& paths,
78                                const std::string& target)
79     : paths(paths), target(target), id("eng") {
80   product = StringFromEnv("TARGET_PRODUCT", "");
81 }
82 
BuildApi(CurlWrapper & curl,CredentialSource * credential_source)83 BuildApi::BuildApi(CurlWrapper& curl, CredentialSource* credential_source)
84     : BuildApi(curl, credential_source, "") {}
85 
BuildApi(CurlWrapper & curl,CredentialSource * credential_source,std::string api_key)86 BuildApi::BuildApi(CurlWrapper& curl, CredentialSource* credential_source,
87                    std::string api_key)
88     : curl(curl),
89       credential_source(credential_source),
90       api_key_(std::move(api_key)) {}
91 
Headers()92 std::vector<std::string> BuildApi::Headers() {
93   std::vector<std::string> headers;
94   if (credential_source) {
95     headers.push_back("Authorization: Bearer " +
96                       credential_source->Credential());
97   }
98   return headers;
99 }
100 
LatestBuildId(const std::string & branch,const std::string & target)101 std::string BuildApi::LatestBuildId(const std::string& branch,
102                                     const std::string& target) {
103   std::string url =
104       BUILD_API + "/builds?branch=" + curl.UrlEscape(branch) +
105       "&buildAttemptStatus=complete" +
106       "&buildType=submitted&maxResults=1&successful=true&target=" +
107       curl.UrlEscape(target);
108   if (!api_key_.empty()) {
109     url += "&key=" + curl.UrlEscape(api_key_);
110   }
111   auto curl_response = curl.DownloadToJson(url, Headers());
112   const auto& json = curl_response.data;
113   if (!curl_response.HttpSuccess()) {
114     LOG(FATAL) << "Error fetching the latest build of \"" << target
115                << "\" on \"" << branch << "\". The server response was \""
116                << json << "\", and code was " << curl_response.http_code;
117   }
118   CHECK(!json.isMember("error"))
119       << "Response had \"error\" but had http success status. Received \""
120       << json << "\"";
121 
122   if (!json.isMember("builds") || json["builds"].size() != 1) {
123     LOG(WARNING) << "expected to receive 1 build for \"" << target << "\" on \""
124                  << branch << "\", but received " << json["builds"].size()
125                  << ". Full response was " << json;
126     return "";
127   }
128   return json["builds"][0]["buildId"].asString();
129 }
130 
BuildStatus(const DeviceBuild & build)131 std::string BuildApi::BuildStatus(const DeviceBuild& build) {
132   std::string url = BUILD_API + "/builds/" + curl.UrlEscape(build.id) + "/" +
133                     curl.UrlEscape(build.target);
134   if (!api_key_.empty()) {
135     url += "?key=" + curl.UrlEscape(api_key_);
136   }
137   auto curl_response = curl.DownloadToJson(url, Headers());
138   const auto& json = curl_response.data;
139   if (!curl_response.HttpSuccess()) {
140     LOG(FATAL) << "Error fetching the status of \"" << build
141                << "\". The server response was \"" << json
142                << "\", and code was " << curl_response.http_code;
143   }
144   CHECK(!json.isMember("error"))
145       << "Response had \"error\" but had http success status. Received \""
146       << json << "\"";
147 
148   return json["buildAttemptStatus"].asString();
149 }
150 
ProductName(const DeviceBuild & build)151 std::string BuildApi::ProductName(const DeviceBuild& build) {
152   std::string url = BUILD_API + "/builds/" + curl.UrlEscape(build.id) + "/" +
153                     curl.UrlEscape(build.target);
154   if (!api_key_.empty()) {
155     url += "?key=" + curl.UrlEscape(api_key_);
156   }
157   auto curl_response = curl.DownloadToJson(url, Headers());
158   const auto& json = curl_response.data;
159   if (!curl_response.HttpSuccess()) {
160     LOG(FATAL) << "Error fetching the product name of \"" << build
161                << "\". The server response was \"" << json
162                << "\", and code was " << curl_response.http_code;
163   }
164   CHECK(!json.isMember("error"))
165       << "Response had \"error\" but had http success status. Received \""
166       << json << "\"";
167 
168   CHECK(json.isMember("target")) << "Build was missing target field.";
169   return json["target"]["product"].asString();
170 }
171 
Artifacts(const DeviceBuild & build)172 std::vector<Artifact> BuildApi::Artifacts(const DeviceBuild& build) {
173   std::string page_token = "";
174   std::vector<Artifact> artifacts;
175   do {
176     std::string url = BUILD_API + "/builds/" + curl.UrlEscape(build.id) + "/" +
177                       curl.UrlEscape(build.target) +
178                       "/attempts/latest/artifacts?maxResults=100";
179     if (page_token != "") {
180       url += "&pageToken=" + curl.UrlEscape(page_token);
181     }
182     if (!api_key_.empty()) {
183       url += "&key=" + curl.UrlEscape(api_key_);
184     }
185     auto curl_response = curl.DownloadToJson(url, Headers());
186     const auto& json = curl_response.data;
187     if (!curl_response.HttpSuccess()) {
188       LOG(FATAL) << "Error fetching the artifacts of \"" << build
189                  << "\". The server response was \"" << json
190                  << "\", and code was " << curl_response.http_code;
191     }
192     CHECK(!json.isMember("error"))
193         << "Response had \"error\" but had http success status. Received \""
194         << json << "\"";
195     if (json.isMember("nextPageToken")) {
196       page_token = json["nextPageToken"].asString();
197     } else {
198       page_token = "";
199     }
200     for (const auto& artifact_json : json["artifacts"]) {
201       artifacts.emplace_back(artifact_json);
202     }
203   } while (page_token != "");
204   return artifacts;
205 }
206 
207 struct CloseDir {
operator ()cuttlefish::CloseDir208   void operator()(DIR* dir) {
209     closedir(dir);
210   }
211 };
212 
213 using UniqueDir = std::unique_ptr<DIR, CloseDir>;
214 
Artifacts(const DirectoryBuild & build)215 std::vector<Artifact> BuildApi::Artifacts(const DirectoryBuild& build) {
216   std::vector<Artifact> artifacts;
217   for (const auto& path : build.paths) {
218     auto dir = UniqueDir(opendir(path.c_str()));
219     CHECK(dir != nullptr) << "Could not read files from \"" << path << "\"";
220     for (auto entity = readdir(dir.get()); entity != nullptr; entity = readdir(dir.get())) {
221       artifacts.emplace_back(std::string(entity->d_name));
222     }
223   }
224   return artifacts;
225 }
226 
ArtifactToCallback(const DeviceBuild & build,const std::string & artifact,CurlWrapper::DataCallback callback)227 bool BuildApi::ArtifactToCallback(const DeviceBuild& build,
228                                   const std::string& artifact,
229                                   CurlWrapper::DataCallback callback) {
230   std::string download_url_endpoint =
231       BUILD_API + "/builds/" + curl.UrlEscape(build.id) + "/" +
232       curl.UrlEscape(build.target) + "/attempts/latest/artifacts/" +
233       curl.UrlEscape(artifact) + "/url";
234   if (!api_key_.empty()) {
235     download_url_endpoint += "?key=" + curl.UrlEscape(api_key_);
236   }
237   auto curl_response = curl.DownloadToJson(download_url_endpoint, Headers());
238   const auto& json = curl_response.data;
239   if (!(curl_response.HttpSuccess() || curl_response.HttpRedirect())) {
240     LOG(ERROR) << "Error fetching the url of \"" << artifact << "\" for \""
241                << build << "\". The server response was \"" << json
242                << "\", and code was " << curl_response.http_code;
243     return false;
244   }
245   if (json.isMember("error")) {
246     LOG(ERROR) << "Response had \"error\" but had http success status. "
247                << "Received \"" << json << "\"";
248     return false;
249   }
250   if (!json.isMember("signedUrl")) {
251     LOG(ERROR) << "URL endpoint did not have json path: " << json;
252     return false;
253   }
254   std::string url = json["signedUrl"].asString();
255   return curl.DownloadToCallback(callback, url).HttpSuccess();
256 }
257 
ArtifactToFile(const DeviceBuild & build,const std::string & artifact,const std::string & path)258 bool BuildApi::ArtifactToFile(const DeviceBuild& build,
259                               const std::string& artifact,
260                               const std::string& path) {
261   std::string download_url_endpoint =
262       BUILD_API + "/builds/" + curl.UrlEscape(build.id) + "/" +
263       curl.UrlEscape(build.target) + "/attempts/latest/artifacts/" +
264       curl.UrlEscape(artifact) + "/url";
265   if (!api_key_.empty()) {
266     download_url_endpoint += "?key=" + curl.UrlEscape(api_key_);
267   }
268   auto curl_response = curl.DownloadToJson(download_url_endpoint, Headers());
269   const auto& json = curl_response.data;
270   if (!(curl_response.HttpSuccess() || curl_response.HttpRedirect())) {
271     LOG(ERROR) << "Error fetching the url of \"" << artifact << "\" for \""
272                << build << "\". The server response was \"" << json
273                << "\", and code was " << curl_response.http_code;
274     return false;
275   }
276   if (json.isMember("error")) {
277     LOG(ERROR) << "Response had \"error\" but had http success status. "
278                << "Received \"" << json << "\"";
279   }
280   if (!json.isMember("signedUrl")) {
281     LOG(ERROR) << "URL endpoint did not have json path: " << json;
282     return false;
283   }
284   std::string url = json["signedUrl"].asString();
285   return curl.DownloadToFile(url, path).HttpSuccess();
286 }
287 
ArtifactToFile(const DirectoryBuild & build,const std::string & artifact,const std::string & destination)288 bool BuildApi::ArtifactToFile(const DirectoryBuild& build,
289                               const std::string& artifact,
290                               const std::string& destination) {
291   for (const auto& path : build.paths) {
292     auto source = path + "/" + artifact;
293     if (!FileExists(source)) {
294       continue;
295     }
296     unlink(destination.c_str());
297     if (symlink(source.c_str(), destination.c_str())) {
298       int error_num = errno;
299       LOG(ERROR) << "Could not create symlink from " << source << " to "
300                   << destination << ": " << strerror(error_num);
301       return false;
302     }
303     return true;
304   }
305   return false;
306 }
307 
ArgumentToBuild(BuildApi * build_api,const std::string & arg,const std::string & default_build_target,const std::chrono::seconds & retry_period)308 Build ArgumentToBuild(BuildApi* build_api, const std::string& arg,
309                       const std::string& default_build_target,
310                       const std::chrono::seconds& retry_period) {
311   if (arg.find(':') != std::string::npos) {
312     std::vector<std::string> dirs = android::base::Split(arg, ":");
313     std::string id = dirs.back();
314     dirs.pop_back();
315     return DirectoryBuild(dirs, id);
316   }
317   size_t slash_pos = arg.find('/');
318   if (slash_pos != std::string::npos
319         && arg.find('/', slash_pos + 1) != std::string::npos) {
320     LOG(FATAL) << "Build argument cannot have more than one '/' slash. Was at "
321         << slash_pos << " and " << arg.find('/', slash_pos + 1);
322   }
323   std::string build_target = slash_pos == std::string::npos
324       ? default_build_target : arg.substr(slash_pos + 1);
325   std::string branch_or_id = slash_pos == std::string::npos
326       ? arg: arg.substr(0, slash_pos);
327   std::string branch_latest_build_id =
328       build_api->LatestBuildId(branch_or_id, build_target);
329   std::string build_id = branch_or_id;
330   if (branch_latest_build_id != "") {
331     LOG(INFO) << "The latest good build on branch \"" << branch_or_id
332         << "\"with build target \"" << build_target
333         << "\" is \"" << branch_latest_build_id << "\"";
334     build_id = branch_latest_build_id;
335   }
336   DeviceBuild proposed_build = DeviceBuild(build_id, build_target);
337   std::string status = build_api->BuildStatus(proposed_build);
338   if (status == "") {
339     LOG(FATAL) << proposed_build << " is not a valid branch or build id.";
340   }
341   LOG(INFO) << "Status for build " << proposed_build << " is " << status;
342   while (retry_period != std::chrono::seconds::zero() && !StatusIsTerminal(status)) {
343     LOG(INFO) << "Status is \"" << status << "\". Waiting for " << retry_period.count()
344         << " seconds.";
345     std::this_thread::sleep_for(retry_period);
346     status = build_api->BuildStatus(proposed_build);
347   }
348   LOG(INFO) << "Status for build " << proposed_build << " is " << status;
349   proposed_build.product = build_api->ProductName(proposed_build);
350   return proposed_build;
351 }
352 
353 } // namespace cuttlefish
354