blob: 517a6b7e98207d52420264d40ed902c822ec6468 [file] [log] [blame]
Julien Desprezd65e6912018-12-13 15:20:52 -08001/*
2 * Copyright (C) 2018 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 */
16package com.android.tradefed.config;
17
18import com.android.annotations.VisibleForTesting;
Julien Desprez63cc5392019-12-05 18:36:44 +000019import com.android.tradefed.build.BuildRetrievalError;
Julien Desprezd65e6912018-12-13 15:20:52 -080020import com.android.tradefed.config.OptionSetter.OptionFieldsForName;
Julien Desprez27bcd4b2018-12-19 12:49:08 -080021import com.android.tradefed.config.remote.GcsRemoteFileResolver;
Julien Desprezfb7b4f62019-07-03 14:05:29 -070022import com.android.tradefed.config.remote.HttpRemoteFileResolver;
23import com.android.tradefed.config.remote.HttpsRemoteFileResolver;
Julien Desprez27bcd4b2018-12-19 12:49:08 -080024import com.android.tradefed.config.remote.IRemoteFileResolver;
Julien Desprez6f8cb8d2019-06-05 10:57:48 -070025import com.android.tradefed.config.remote.LocalFileResolver;
Julien Desprezd65e6912018-12-13 15:20:52 -080026import com.android.tradefed.log.LogUtil.CLog;
27import com.android.tradefed.util.FileUtil;
Julien Despreze30f0d92019-01-07 17:32:14 -080028import com.android.tradefed.util.MultiMap;
Julien Desprezc18493d2019-06-13 09:49:29 -070029import com.android.tradefed.util.ZipUtil;
30import com.android.tradefed.util.ZipUtil2;
Julien Desprezd65e6912018-12-13 15:20:52 -080031
32import java.io.File;
Julien Desprezc18493d2019-06-13 09:49:29 -070033import java.io.IOException;
Julien Desprezd65e6912018-12-13 15:20:52 -080034import java.lang.reflect.Field;
Julien Desprez13138de2019-06-10 16:57:19 -070035import java.net.URI;
36import java.net.URISyntaxException;
Julien Desprezd65e6912018-12-13 15:20:52 -080037import java.util.ArrayList;
38import java.util.Collection;
Julien Desprez27bcd4b2018-12-19 12:49:08 -080039import java.util.HashMap;
Julien Desprezd65e6912018-12-13 15:20:52 -080040import java.util.HashSet;
Julien Desprez8f0432f2019-01-04 13:47:54 -080041import java.util.LinkedHashMap;
Julien Despreze30f0d92019-01-07 17:32:14 -080042import java.util.List;
Julien Desprezd65e6912018-12-13 15:20:52 -080043import java.util.Map;
Julien Desprez8f0432f2019-01-04 13:47:54 -080044import java.util.Map.Entry;
Julien Desprezd65e6912018-12-13 15:20:52 -080045import java.util.Set;
Julien Desprez15d9c7d2019-01-02 11:15:18 -080046import java.util.concurrent.atomic.AtomicBoolean;
Julien Desprezd65e6912018-12-13 15:20:52 -080047
48/**
49 * Class that helps resolving path to remote files.
50 *
51 * <p>For example: gs://bucket/path/file.txt will be resolved by downloading the file from the GCS
52 * bucket.
Julien Desprezd65e6912018-12-13 15:20:52 -080053 */
54public class DynamicRemoteFileResolver {
55
Julien Desprez15d9c7d2019-01-02 11:15:18 -080056 public static final String DYNAMIC_RESOLVER = "dynamic-resolver";
Julien Desprez27bcd4b2018-12-19 12:49:08 -080057 private static final Map<String, IRemoteFileResolver> PROTOCOL_SUPPORT = new HashMap<>();
58
59 static {
Julien Desprez27bcd4b2018-12-19 12:49:08 -080060 PROTOCOL_SUPPORT.put(GcsRemoteFileResolver.PROTOCOL, new GcsRemoteFileResolver());
Julien Desprez6f8cb8d2019-06-05 10:57:48 -070061 PROTOCOL_SUPPORT.put(LocalFileResolver.PROTOCOL, new LocalFileResolver());
Julien Desprezfb7b4f62019-07-03 14:05:29 -070062 PROTOCOL_SUPPORT.put(HttpRemoteFileResolver.PROTOCOL_HTTP, new HttpRemoteFileResolver());
63 PROTOCOL_SUPPORT.put(HttpsRemoteFileResolver.PROTOCOL_HTTPS, new HttpsRemoteFileResolver());
Julien Desprez27bcd4b2018-12-19 12:49:08 -080064 }
Julien Desprez15d9c7d2019-01-02 11:15:18 -080065 // The configuration map being static, we only need to update it once per TF instance.
66 private static AtomicBoolean sIsUpdateDone = new AtomicBoolean(false);
Julien Desprezc18493d2019-06-13 09:49:29 -070067 // Query key for requesting to unzip a downloaded file automatically.
68 public static final String UNZIP_KEY = "unzip";
Julien Despreza0a4aa72019-06-18 11:36:22 -070069 // Query key for requesting a download to be optional, so if it fails we don't replace it.
70 public static final String OPTIONAL_KEY = "optional";
Julien Desprez27bcd4b2018-12-19 12:49:08 -080071
Julien Desprezd65e6912018-12-13 15:20:52 -080072 private Map<String, OptionFieldsForName> mOptionMap;
73
74 /** Sets the map of options coming from {@link OptionSetter} */
75 public void setOptionMap(Map<String, OptionFieldsForName> optionMap) {
76 mOptionMap = optionMap;
77 }
78
79 /**
80 * Runs through all the {@link File} option type and check if their path should be resolved.
81 *
82 * @return The list of {@link File} that was resolved that way.
Julien Desprez63cc5392019-12-05 18:36:44 +000083 * @throws BuildRetrievalError
Julien Desprezd65e6912018-12-13 15:20:52 -080084 */
Julien Desprez63cc5392019-12-05 18:36:44 +000085 public final Set<File> validateRemoteFilePath() throws BuildRetrievalError {
Julien Desprezd65e6912018-12-13 15:20:52 -080086 Set<File> downloadedFiles = new HashSet<>();
87 try {
Julien Desprezcf0f0b22019-09-18 14:34:28 -070088 Map<Field, Object> fieldSeen = new HashMap<>();
Julien Desprezd65e6912018-12-13 15:20:52 -080089 for (Map.Entry<String, OptionFieldsForName> optionPair : mOptionMap.entrySet()) {
Julien Desprezd65e6912018-12-13 15:20:52 -080090 final OptionFieldsForName optionFields = optionPair.getValue();
Julien Desprezd65e6912018-12-13 15:20:52 -080091 for (Map.Entry<Object, Field> fieldEntry : optionFields) {
Julien Despreza0a4aa72019-06-18 11:36:22 -070092
Julien Desprezd65e6912018-12-13 15:20:52 -080093 final Object obj = fieldEntry.getKey();
94 final Field field = fieldEntry.getValue();
95 final Option option = field.getAnnotation(Option.class);
96 if (option == null) {
97 continue;
98 }
99 // At this point, we know this is an option field; make sure it's set
100 field.setAccessible(true);
101 final Object value;
102 try {
103 value = field.get(obj);
Julien Desprezc0541882019-07-10 08:36:02 -0700104 if (value == null) {
105 continue;
106 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800107 } catch (IllegalAccessException e) {
Julien Desprez63cc5392019-12-05 18:36:44 +0000108 throw new BuildRetrievalError(
Julien Desprezd65e6912018-12-13 15:20:52 -0800109 String.format("internal error: %s", e.getMessage()));
110 }
111
Julien Desprezcf0f0b22019-09-18 14:34:28 -0700112 if (fieldSeen.get(field) != null && fieldSeen.get(field).equals(obj)) {
Julien Desprezd65e6912018-12-13 15:20:52 -0800113 continue;
Julien Desprezc0541882019-07-10 08:36:02 -0700114 }
115 // Keep track of the field set on each object
Julien Desprezcf0f0b22019-09-18 14:34:28 -0700116 fieldSeen.put(field, obj);
Julien Desprezc0541882019-07-10 08:36:02 -0700117
118 if (value instanceof File) {
Julien Desprezd65e6912018-12-13 15:20:52 -0800119 File consideredFile = (File) value;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800120 File downloadedFile = resolveRemoteFiles(consideredFile, option);
Julien Desprezd65e6912018-12-13 15:20:52 -0800121 if (downloadedFile != null) {
122 downloadedFiles.add(downloadedFile);
123 // Replace the field value
124 try {
125 field.set(obj, downloadedFile);
126 } catch (IllegalAccessException e) {
127 CLog.e(e);
Julien Desprez63cc5392019-12-05 18:36:44 +0000128 throw new BuildRetrievalError(
Julien Desprezd65e6912018-12-13 15:20:52 -0800129 String.format(
Julien Desprez65249662018-12-27 16:50:29 -0800130 "Failed to download %s due to '%s'",
131 consideredFile.getPath(), e.getMessage()),
Julien Desprezd65e6912018-12-13 15:20:52 -0800132 e);
133 }
134 }
135 } else if (value instanceof Collection) {
136 Collection<Object> c = (Collection<Object>) value;
137 Collection<Object> copy = new ArrayList<>(c);
138 for (Object o : copy) {
139 if (o instanceof File) {
140 File consideredFile = (File) o;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800141 File downloadedFile = resolveRemoteFiles(consideredFile, option);
Julien Desprezd65e6912018-12-13 15:20:52 -0800142 if (downloadedFile != null) {
143 downloadedFiles.add(downloadedFile);
144 // TODO: See if order could be preserved.
145 c.remove(consideredFile);
146 c.add(downloadedFile);
147 }
148 }
149 }
Julien Desprez8f0432f2019-01-04 13:47:54 -0800150 } else if (value instanceof Map) {
151 Map<Object, Object> m = (Map<Object, Object>) value;
152 Map<Object, Object> copy = new LinkedHashMap<>(m);
153 for (Entry<Object, Object> entry : copy.entrySet()) {
154 Object key = entry.getKey();
155 Object val = entry.getValue();
156
157 Object finalKey = key;
158 Object finalVal = val;
159 if (key instanceof File) {
160 key = resolveRemoteFiles((File) key, option);
161 if (key != null) {
162 downloadedFiles.add((File) key);
163 finalKey = key;
164 }
165 }
166 if (val instanceof File) {
167 val = resolveRemoteFiles((File) val, option);
168 if (val != null) {
169 downloadedFiles.add((File) val);
170 finalVal = val;
171 }
172 }
173
174 m.remove(entry.getKey());
175 m.put(finalKey, finalVal);
176 }
Julien Despreze30f0d92019-01-07 17:32:14 -0800177 } else if (value instanceof MultiMap) {
178 MultiMap<Object, Object> m = (MultiMap<Object, Object>) value;
179 MultiMap<Object, Object> copy = new MultiMap<>(m);
180 for (Object key : copy.keySet()) {
181 List<Object> mapValues = copy.get(key);
182
183 m.remove(key);
184 Object finalKey = key;
185 if (key instanceof File) {
186 key = resolveRemoteFiles((File) key, option);
187 if (key != null) {
188 downloadedFiles.add((File) key);
189 finalKey = key;
190 }
191 }
192 for (Object mapValue : mapValues) {
193 if (mapValue instanceof File) {
194 File f = resolveRemoteFiles((File) mapValue, option);
195 if (f != null) {
196 downloadedFiles.add(f);
197 mapValue = f;
198 }
199 }
200 m.put(finalKey, mapValue);
201 }
202 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800203 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800204 }
205 }
Julien Desprez63cc5392019-12-05 18:36:44 +0000206 } catch (BuildRetrievalError e) {
Julien Desprezd65e6912018-12-13 15:20:52 -0800207 // Clean up the files before throwing
208 for (File f : downloadedFiles) {
209 FileUtil.recursiveDelete(f);
210 }
211 throw e;
212 }
213 return downloadedFiles;
214 }
215
Dan Shif663f592019-06-25 16:17:16 -0700216 /**
217 * Download the files matching given filters in a remote zip file.
218 *
219 * <p>A file inside the remote zip file is only downloaded if its path matches any of the
220 * include filters but not the exclude filters.
221 *
222 * @param destDir the file to place the downloaded contents into.
223 * @param remoteZipFilePath the remote path to the zip file to download, relative to an
224 * implementation specific root.
225 * @param includeFilters a list of regex strings to download matching files. A file's path
226 * matching any filter will be downloaded.
227 * @param excludeFilters a list of regex strings to skip downloading matching files. A file's
228 * path matching any filter will not be downloaded.
Julien Desprez63cc5392019-12-05 18:36:44 +0000229 * @throws BuildRetrievalError if files could not be downloaded.
Dan Shif663f592019-06-25 16:17:16 -0700230 */
231 public void resolvePartialDownloadZip(
232 File destDir,
233 String remoteZipFilePath,
234 List<String> includeFilters,
235 List<String> excludeFilters)
Julien Desprez63cc5392019-12-05 18:36:44 +0000236 throws BuildRetrievalError {
Dan Shif663f592019-06-25 16:17:16 -0700237 Map<String, String> queryArgs;
238 String protocol;
239 try {
240 URI uri = new URI(remoteZipFilePath);
241 protocol = uri.getScheme();
242 queryArgs = parseQuery(uri.getQuery());
243 } catch (URISyntaxException e) {
Julien Desprez63cc5392019-12-05 18:36:44 +0000244 throw new BuildRetrievalError(
Dan Shif663f592019-06-25 16:17:16 -0700245 String.format(
246 "Failed to parse the remote zip file path: %s", remoteZipFilePath),
247 e);
248 }
249 IRemoteFileResolver resolver = getResolver(protocol);
250
251 queryArgs.put("partial_download_dir", destDir.getAbsolutePath());
252 if (includeFilters != null) {
253 queryArgs.put("include_filters", String.join(";", includeFilters));
254 }
255 if (excludeFilters != null) {
256 queryArgs.put("exclude_filters", String.join(";", excludeFilters));
257 }
258 // Downloaded individual files should be saved to destDir, return value is not needed.
259 try {
Julien Desprez3b318ad2020-01-13 10:45:12 -0800260 resolver.resolveRemoteFiles(new File(remoteZipFilePath), queryArgs);
Julien Desprez63cc5392019-12-05 18:36:44 +0000261 } catch (BuildRetrievalError e) {
Dan Shif663f592019-06-25 16:17:16 -0700262 if (isOptional(queryArgs)) {
263 CLog.d(
264 "Failed to partially download '%s' but marked optional so skipping: %s",
265 remoteZipFilePath, e.getMessage());
266 } else {
267 throw e;
268 }
269 }
270 }
271
Julien Desprezd65e6912018-12-13 15:20:52 -0800272 @VisibleForTesting
Julien Desprez9b477232019-03-07 11:08:25 -0800273 protected IRemoteFileResolver getResolver(String protocol) {
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800274 if (updateProtocols()) {
275 IGlobalConfiguration globalConfig = getGlobalConfig();
276 Object o = globalConfig.getConfigurationObject(DYNAMIC_RESOLVER);
277 if (o != null) {
278 if (o instanceof IRemoteFileResolver) {
279 IRemoteFileResolver resolver = (IRemoteFileResolver) o;
280 CLog.d("Adding %s to supported remote file resolver", resolver);
281 PROTOCOL_SUPPORT.put(resolver.getSupportedProtocol(), resolver);
282 } else {
283 CLog.e("%s is not of type IRemoteFileResolver", o);
284 }
285 }
286 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800287 return PROTOCOL_SUPPORT.get(protocol);
Julien Desprezd65e6912018-12-13 15:20:52 -0800288 }
289
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800290 @VisibleForTesting
Julien Desprez9b477232019-03-07 11:08:25 -0800291 protected boolean updateProtocols() {
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800292 return sIsUpdateDone.compareAndSet(false, true);
293 }
294
295 @VisibleForTesting
296 IGlobalConfiguration getGlobalConfig() {
297 return GlobalConfiguration.getInstance();
298 }
299
Julien Desprezc18493d2019-06-13 09:49:29 -0700300 /**
301 * Utility that allows to check whether or not a file should be unzip and unzip it if required.
302 */
303 public static final File unzipIfRequired(File downloadedFile, Map<String, String> query)
304 throws IOException {
305 String unzipValue = query.get(UNZIP_KEY);
306 if (unzipValue != null && "true".equals(unzipValue.toLowerCase())) {
307 // File was requested to be unzipped.
308 if (ZipUtil.isZipFileValid(downloadedFile, false)) {
309 File unzipped =
310 ZipUtil2.extractZipToTemp(
311 downloadedFile, FileUtil.getBaseName(downloadedFile.getName()));
312 FileUtil.deleteFile(downloadedFile);
313 return unzipped;
314 } else {
315 CLog.w("%s was requested to be unzipped but is not a valid zip.", downloadedFile);
316 }
317 }
318 // Return the original file untouched
319 return downloadedFile;
320 }
321
Julien Desprez63cc5392019-12-05 18:36:44 +0000322 private File resolveRemoteFiles(File consideredFile, Option option) throws BuildRetrievalError {
Julien Desprez13138de2019-06-10 16:57:19 -0700323 File fileToResolve;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800324 String path = consideredFile.getPath();
Julien Desprez13138de2019-06-10 16:57:19 -0700325 String protocol;
326 Map<String, String> query;
327 try {
328 URI uri = new URI(path);
329 protocol = uri.getScheme();
330 query = parseQuery(uri.getQuery());
331 fileToResolve = new File(protocol + ":" + uri.getPath());
332 } catch (URISyntaxException e) {
333 CLog.e(e);
334 return null;
335 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800336 IRemoteFileResolver resolver = getResolver(protocol);
337 if (resolver != null) {
Julien Despreza0a4aa72019-06-18 11:36:22 -0700338 try {
Julien Desprez9b9baf82019-12-17 11:43:33 -0800339 CLog.d(
340 "Considering option '%s' with path: '%s' for download.",
341 option.name(), path);
Julien Desprez3b318ad2020-01-13 10:45:12 -0800342 return resolver.resolveRemoteFiles(fileToResolve, query);
Julien Desprez63cc5392019-12-05 18:36:44 +0000343 } catch (BuildRetrievalError e) {
Julien Despreza0a4aa72019-06-18 11:36:22 -0700344 if (isOptional(query)) {
345 CLog.d(
346 "Failed to resolve '%s' but marked optional so skipping: %s",
347 fileToResolve, e.getMessage());
348 } else {
349 throw e;
350 }
351 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800352 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800353 // Not a remote file
Julien Desprezd65e6912018-12-13 15:20:52 -0800354 return null;
355 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800356
357 /**
Julien Desprez13138de2019-06-10 16:57:19 -0700358 * Parse a URL query style. Delimited by &, and map values represented by =. Example:
359 * ?key=value&key2=value2
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800360 */
Julien Desprez13138de2019-06-10 16:57:19 -0700361 private Map<String, String> parseQuery(String query) {
362 Map<String, String> values = new HashMap<>();
363 if (query == null) {
364 return values;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800365 }
Julien Desprez13138de2019-06-10 16:57:19 -0700366 for (String maps : query.split("&")) {
367 String[] keyVal = maps.split("=");
368 values.put(keyVal[0], keyVal[1]);
369 }
370 return values;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800371 }
Julien Despreza0a4aa72019-06-18 11:36:22 -0700372
373 /** Whether or not a link was requested as optional. */
374 private boolean isOptional(Map<String, String> query) {
375 String value = query.get(OPTIONAL_KEY);
376 if (value == null) {
377 return false;
378 }
379 return "true".equals(value.toLowerCase());
380 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800381}