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