blob: fdf69f52b9b8b3cfc428770815f4d50c819861bc [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 Desprez2d01ae32020-02-27 09:46:23 -080026import com.android.tradefed.device.ITestDevice;
Julien Desprezd65e6912018-12-13 15:20:52 -080027import com.android.tradefed.log.LogUtil.CLog;
28import com.android.tradefed.util.FileUtil;
Julien Despreze30f0d92019-01-07 17:32:14 -080029import com.android.tradefed.util.MultiMap;
Julien Desprezc18493d2019-06-13 09:49:29 -070030import com.android.tradefed.util.ZipUtil;
31import com.android.tradefed.util.ZipUtil2;
Julien Desprezd65e6912018-12-13 15:20:52 -080032
33import java.io.File;
Julien Desprezc18493d2019-06-13 09:49:29 -070034import java.io.IOException;
Julien Desprezd65e6912018-12-13 15:20:52 -080035import java.lang.reflect.Field;
Julien Desprez13138de2019-06-10 16:57:19 -070036import java.net.URI;
37import java.net.URISyntaxException;
Julien Desprezd65e6912018-12-13 15:20:52 -080038import java.util.ArrayList;
39import java.util.Collection;
Julien Desprez27bcd4b2018-12-19 12:49:08 -080040import java.util.HashMap;
Julien Desprezd65e6912018-12-13 15:20:52 -080041import java.util.HashSet;
Julien Desprez8f0432f2019-01-04 13:47:54 -080042import java.util.LinkedHashMap;
Julien Despreze30f0d92019-01-07 17:32:14 -080043import java.util.List;
Julien Desprezd65e6912018-12-13 15:20:52 -080044import java.util.Map;
Julien Desprez8f0432f2019-01-04 13:47:54 -080045import java.util.Map.Entry;
Julien Despreza71befa2020-02-10 14:24:17 -080046import java.util.ServiceLoader;
Julien Desprezd65e6912018-12-13 15:20:52 -080047import java.util.Set;
Julien Desprez15d9c7d2019-01-02 11:15:18 -080048import java.util.concurrent.atomic.AtomicBoolean;
Julien Desprezd65e6912018-12-13 15:20:52 -080049
50/**
51 * Class that helps resolving path to remote files.
52 *
53 * <p>For example: gs://bucket/path/file.txt will be resolved by downloading the file from the GCS
54 * bucket.
Julien Despreza71befa2020-02-10 14:24:17 -080055 *
56 * <p>New protocols should be added to META_INF/services.
Julien Desprezd65e6912018-12-13 15:20:52 -080057 */
58public class DynamicRemoteFileResolver {
59
Julien Desprez27bcd4b2018-12-19 12:49:08 -080060 private static final Map<String, IRemoteFileResolver> PROTOCOL_SUPPORT = new HashMap<>();
61
62 static {
Julien Desprez27bcd4b2018-12-19 12:49:08 -080063 PROTOCOL_SUPPORT.put(GcsRemoteFileResolver.PROTOCOL, new GcsRemoteFileResolver());
Julien Desprez6f8cb8d2019-06-05 10:57:48 -070064 PROTOCOL_SUPPORT.put(LocalFileResolver.PROTOCOL, new LocalFileResolver());
Julien Desprezfb7b4f62019-07-03 14:05:29 -070065 PROTOCOL_SUPPORT.put(HttpRemoteFileResolver.PROTOCOL_HTTP, new HttpRemoteFileResolver());
66 PROTOCOL_SUPPORT.put(HttpsRemoteFileResolver.PROTOCOL_HTTPS, new HttpsRemoteFileResolver());
Julien Desprez27bcd4b2018-12-19 12:49:08 -080067 }
Julien Desprez15d9c7d2019-01-02 11:15:18 -080068 // The configuration map being static, we only need to update it once per TF instance.
69 private static AtomicBoolean sIsUpdateDone = new AtomicBoolean(false);
Julien Desprezc18493d2019-06-13 09:49:29 -070070 // Query key for requesting to unzip a downloaded file automatically.
71 public static final String UNZIP_KEY = "unzip";
Julien Despreza0a4aa72019-06-18 11:36:22 -070072 // Query key for requesting a download to be optional, so if it fails we don't replace it.
73 public static final String OPTIONAL_KEY = "optional";
Julien Desprez27bcd4b2018-12-19 12:49:08 -080074
Julien Desprezd65e6912018-12-13 15:20:52 -080075 private Map<String, OptionFieldsForName> mOptionMap;
Julien Desprez28f4af12020-02-28 11:30:00 -080076 // Populated from {@link ICommandOptions#getDynamicDownloadArgs()}
77 private Map<String, String> mExtraArgs = new LinkedHashMap<>();
Julien Desprez2d01ae32020-02-27 09:46:23 -080078 private ITestDevice mDevice;
Julien Desprezd65e6912018-12-13 15:20:52 -080079
80 /** Sets the map of options coming from {@link OptionSetter} */
81 public void setOptionMap(Map<String, OptionFieldsForName> optionMap) {
82 mOptionMap = optionMap;
83 }
84
Julien Desprez2d01ae32020-02-27 09:46:23 -080085 /** Sets the device under tests */
86 public void setDevice(ITestDevice device) {
87 mDevice = device;
88 }
89
Julien Desprez28f4af12020-02-28 11:30:00 -080090 /** Add extra args for the query. */
91 public void addExtraArgs(Map<String, String> extraArgs) {
92 mExtraArgs.putAll(extraArgs);
93 }
94
Julien Desprezd65e6912018-12-13 15:20:52 -080095 /**
96 * Runs through all the {@link File} option type and check if their path should be resolved.
97 *
98 * @return The list of {@link File} that was resolved that way.
Julien Desprez63cc5392019-12-05 18:36:44 +000099 * @throws BuildRetrievalError
Julien Desprezd65e6912018-12-13 15:20:52 -0800100 */
Julien Desprez63cc5392019-12-05 18:36:44 +0000101 public final Set<File> validateRemoteFilePath() throws BuildRetrievalError {
Julien Desprezd65e6912018-12-13 15:20:52 -0800102 Set<File> downloadedFiles = new HashSet<>();
103 try {
Julien Desprezcf0f0b22019-09-18 14:34:28 -0700104 Map<Field, Object> fieldSeen = new HashMap<>();
Julien Desprezd65e6912018-12-13 15:20:52 -0800105 for (Map.Entry<String, OptionFieldsForName> optionPair : mOptionMap.entrySet()) {
Julien Desprezd65e6912018-12-13 15:20:52 -0800106 final OptionFieldsForName optionFields = optionPair.getValue();
Julien Desprezd65e6912018-12-13 15:20:52 -0800107 for (Map.Entry<Object, Field> fieldEntry : optionFields) {
Julien Despreza0a4aa72019-06-18 11:36:22 -0700108
Julien Desprezd65e6912018-12-13 15:20:52 -0800109 final Object obj = fieldEntry.getKey();
110 final Field field = fieldEntry.getValue();
111 final Option option = field.getAnnotation(Option.class);
112 if (option == null) {
113 continue;
114 }
115 // At this point, we know this is an option field; make sure it's set
116 field.setAccessible(true);
117 final Object value;
118 try {
119 value = field.get(obj);
Julien Desprezc0541882019-07-10 08:36:02 -0700120 if (value == null) {
121 continue;
122 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800123 } catch (IllegalAccessException e) {
Julien Desprez63cc5392019-12-05 18:36:44 +0000124 throw new BuildRetrievalError(
Julien Desprezd65e6912018-12-13 15:20:52 -0800125 String.format("internal error: %s", e.getMessage()));
126 }
127
Julien Desprezcf0f0b22019-09-18 14:34:28 -0700128 if (fieldSeen.get(field) != null && fieldSeen.get(field).equals(obj)) {
Julien Desprezd65e6912018-12-13 15:20:52 -0800129 continue;
Julien Desprezc0541882019-07-10 08:36:02 -0700130 }
131 // Keep track of the field set on each object
Julien Desprezcf0f0b22019-09-18 14:34:28 -0700132 fieldSeen.put(field, obj);
Julien Desprezc0541882019-07-10 08:36:02 -0700133
134 if (value instanceof File) {
Julien Desprezd65e6912018-12-13 15:20:52 -0800135 File consideredFile = (File) value;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800136 File downloadedFile = resolveRemoteFiles(consideredFile, option);
Julien Desprezd65e6912018-12-13 15:20:52 -0800137 if (downloadedFile != null) {
138 downloadedFiles.add(downloadedFile);
139 // Replace the field value
140 try {
141 field.set(obj, downloadedFile);
142 } catch (IllegalAccessException e) {
143 CLog.e(e);
Julien Desprez63cc5392019-12-05 18:36:44 +0000144 throw new BuildRetrievalError(
Julien Desprezd65e6912018-12-13 15:20:52 -0800145 String.format(
Julien Desprez65249662018-12-27 16:50:29 -0800146 "Failed to download %s due to '%s'",
147 consideredFile.getPath(), e.getMessage()),
Julien Desprezd65e6912018-12-13 15:20:52 -0800148 e);
149 }
150 }
151 } else if (value instanceof Collection) {
152 Collection<Object> c = (Collection<Object>) value;
153 Collection<Object> copy = new ArrayList<>(c);
154 for (Object o : copy) {
155 if (o instanceof File) {
156 File consideredFile = (File) o;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800157 File downloadedFile = resolveRemoteFiles(consideredFile, option);
Julien Desprezd65e6912018-12-13 15:20:52 -0800158 if (downloadedFile != null) {
159 downloadedFiles.add(downloadedFile);
160 // TODO: See if order could be preserved.
161 c.remove(consideredFile);
162 c.add(downloadedFile);
163 }
164 }
165 }
Julien Desprez8f0432f2019-01-04 13:47:54 -0800166 } else if (value instanceof Map) {
167 Map<Object, Object> m = (Map<Object, Object>) value;
168 Map<Object, Object> copy = new LinkedHashMap<>(m);
169 for (Entry<Object, Object> entry : copy.entrySet()) {
170 Object key = entry.getKey();
171 Object val = entry.getValue();
172
173 Object finalKey = key;
174 Object finalVal = val;
175 if (key instanceof File) {
176 key = resolveRemoteFiles((File) key, option);
177 if (key != null) {
178 downloadedFiles.add((File) key);
179 finalKey = key;
180 }
181 }
182 if (val instanceof File) {
183 val = resolveRemoteFiles((File) val, option);
184 if (val != null) {
185 downloadedFiles.add((File) val);
186 finalVal = val;
187 }
188 }
189
190 m.remove(entry.getKey());
191 m.put(finalKey, finalVal);
192 }
Julien Despreze30f0d92019-01-07 17:32:14 -0800193 } else if (value instanceof MultiMap) {
194 MultiMap<Object, Object> m = (MultiMap<Object, Object>) value;
Julien Despreze218c722020-03-27 13:34:45 -0700195 synchronized (m) {
196 MultiMap<Object, Object> copy = new MultiMap<>(m);
197 for (Object key : copy.keySet()) {
198 List<Object> mapValues = copy.get(key);
Julien Despreze30f0d92019-01-07 17:32:14 -0800199
Julien Despreze218c722020-03-27 13:34:45 -0700200 m.remove(key);
201 Object finalKey = key;
202 if (key instanceof File) {
203 key = resolveRemoteFiles((File) key, option);
204 if (key != null) {
205 downloadedFiles.add((File) key);
206 finalKey = key;
Julien Despreze30f0d92019-01-07 17:32:14 -0800207 }
208 }
Julien Despreze218c722020-03-27 13:34:45 -0700209 for (Object mapValue : mapValues) {
210 if (mapValue instanceof File) {
211 File f = resolveRemoteFiles((File) mapValue, option);
212 if (f != null) {
213 downloadedFiles.add(f);
214 mapValue = f;
215 }
216 }
217 m.put(finalKey, mapValue);
218 }
Julien Despreze30f0d92019-01-07 17:32:14 -0800219 }
220 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800221 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800222 }
223 }
Julien Despreze218c722020-03-27 13:34:45 -0700224 } catch (RuntimeException | BuildRetrievalError e) {
Julien Desprezd65e6912018-12-13 15:20:52 -0800225 // Clean up the files before throwing
226 for (File f : downloadedFiles) {
227 FileUtil.recursiveDelete(f);
228 }
229 throw e;
230 }
231 return downloadedFiles;
232 }
233
Dan Shif663f592019-06-25 16:17:16 -0700234 /**
235 * Download the files matching given filters in a remote zip file.
236 *
237 * <p>A file inside the remote zip file is only downloaded if its path matches any of the
238 * include filters but not the exclude filters.
239 *
240 * @param destDir the file to place the downloaded contents into.
241 * @param remoteZipFilePath the remote path to the zip file to download, relative to an
242 * implementation specific root.
243 * @param includeFilters a list of regex strings to download matching files. A file's path
244 * matching any filter will be downloaded.
245 * @param excludeFilters a list of regex strings to skip downloading matching files. A file's
246 * path matching any filter will not be downloaded.
Julien Desprez63cc5392019-12-05 18:36:44 +0000247 * @throws BuildRetrievalError if files could not be downloaded.
Dan Shif663f592019-06-25 16:17:16 -0700248 */
249 public void resolvePartialDownloadZip(
250 File destDir,
251 String remoteZipFilePath,
252 List<String> includeFilters,
253 List<String> excludeFilters)
Julien Desprez63cc5392019-12-05 18:36:44 +0000254 throws BuildRetrievalError {
Dan Shif663f592019-06-25 16:17:16 -0700255 Map<String, String> queryArgs;
256 String protocol;
257 try {
258 URI uri = new URI(remoteZipFilePath);
259 protocol = uri.getScheme();
260 queryArgs = parseQuery(uri.getQuery());
261 } catch (URISyntaxException e) {
Julien Desprez63cc5392019-12-05 18:36:44 +0000262 throw new BuildRetrievalError(
Dan Shif663f592019-06-25 16:17:16 -0700263 String.format(
264 "Failed to parse the remote zip file path: %s", remoteZipFilePath),
265 e);
266 }
267 IRemoteFileResolver resolver = getResolver(protocol);
268
269 queryArgs.put("partial_download_dir", destDir.getAbsolutePath());
270 if (includeFilters != null) {
271 queryArgs.put("include_filters", String.join(";", includeFilters));
272 }
273 if (excludeFilters != null) {
274 queryArgs.put("exclude_filters", String.join(";", excludeFilters));
275 }
276 // Downloaded individual files should be saved to destDir, return value is not needed.
277 try {
Julien Desprez2d01ae32020-02-27 09:46:23 -0800278 resolver.setPrimaryDevice(mDevice);
Julien Desprez3b318ad2020-01-13 10:45:12 -0800279 resolver.resolveRemoteFiles(new File(remoteZipFilePath), queryArgs);
Julien Desprez63cc5392019-12-05 18:36:44 +0000280 } catch (BuildRetrievalError e) {
Dan Shif663f592019-06-25 16:17:16 -0700281 if (isOptional(queryArgs)) {
282 CLog.d(
283 "Failed to partially download '%s' but marked optional so skipping: %s",
284 remoteZipFilePath, e.getMessage());
285 } else {
286 throw e;
287 }
288 }
289 }
290
Julien Desprezd65e6912018-12-13 15:20:52 -0800291 @VisibleForTesting
Julien Desprez9b477232019-03-07 11:08:25 -0800292 protected IRemoteFileResolver getResolver(String protocol) {
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800293 if (updateProtocols()) {
Julien Despreza71befa2020-02-10 14:24:17 -0800294 // Use the service loader to find all the implementations.
295 ServiceLoader<IRemoteFileResolver> serviceLoader =
296 ServiceLoader.load(IRemoteFileResolver.class);
297 for (IRemoteFileResolver resolver : serviceLoader) {
298 PROTOCOL_SUPPORT.putIfAbsent(resolver.getSupportedProtocol(), resolver);
299 }
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800300 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800301 return PROTOCOL_SUPPORT.get(protocol);
Julien Desprezd65e6912018-12-13 15:20:52 -0800302 }
303
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800304 @VisibleForTesting
Julien Desprez9b477232019-03-07 11:08:25 -0800305 protected boolean updateProtocols() {
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800306 return sIsUpdateDone.compareAndSet(false, true);
307 }
308
309 @VisibleForTesting
310 IGlobalConfiguration getGlobalConfig() {
311 return GlobalConfiguration.getInstance();
312 }
313
Julien Desprezc18493d2019-06-13 09:49:29 -0700314 /**
315 * Utility that allows to check whether or not a file should be unzip and unzip it if required.
316 */
317 public static final File unzipIfRequired(File downloadedFile, Map<String, String> query)
318 throws IOException {
319 String unzipValue = query.get(UNZIP_KEY);
320 if (unzipValue != null && "true".equals(unzipValue.toLowerCase())) {
321 // File was requested to be unzipped.
322 if (ZipUtil.isZipFileValid(downloadedFile, false)) {
323 File unzipped =
324 ZipUtil2.extractZipToTemp(
325 downloadedFile, FileUtil.getBaseName(downloadedFile.getName()));
326 FileUtil.deleteFile(downloadedFile);
327 return unzipped;
328 } else {
329 CLog.w("%s was requested to be unzipped but is not a valid zip.", downloadedFile);
330 }
331 }
332 // Return the original file untouched
333 return downloadedFile;
334 }
335
Julien Desprez63cc5392019-12-05 18:36:44 +0000336 private File resolveRemoteFiles(File consideredFile, Option option) throws BuildRetrievalError {
Julien Desprez13138de2019-06-10 16:57:19 -0700337 File fileToResolve;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800338 String path = consideredFile.getPath();
Julien Desprez13138de2019-06-10 16:57:19 -0700339 String protocol;
340 Map<String, String> query;
341 try {
342 URI uri = new URI(path);
343 protocol = uri.getScheme();
344 query = parseQuery(uri.getQuery());
345 fileToResolve = new File(protocol + ":" + uri.getPath());
346 } catch (URISyntaxException e) {
347 CLog.e(e);
348 return null;
349 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800350 IRemoteFileResolver resolver = getResolver(protocol);
351 if (resolver != null) {
Julien Despreza0a4aa72019-06-18 11:36:22 -0700352 try {
Julien Desprez9b9baf82019-12-17 11:43:33 -0800353 CLog.d(
354 "Considering option '%s' with path: '%s' for download.",
355 option.name(), path);
Julien Desprez28f4af12020-02-28 11:30:00 -0800356 // Overrides query args
357 query.putAll(mExtraArgs);
Hadrien Zalek536f8462020-05-02 00:45:47 -0700358 resolver.setPrimaryDevice(mDevice);
Julien Desprez3b318ad2020-01-13 10:45:12 -0800359 return resolver.resolveRemoteFiles(fileToResolve, query);
Julien Desprez63cc5392019-12-05 18:36:44 +0000360 } catch (BuildRetrievalError e) {
Julien Despreza0a4aa72019-06-18 11:36:22 -0700361 if (isOptional(query)) {
362 CLog.d(
363 "Failed to resolve '%s' but marked optional so skipping: %s",
364 fileToResolve, e.getMessage());
365 } else {
366 throw e;
367 }
368 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800369 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800370 // Not a remote file
Julien Desprezd65e6912018-12-13 15:20:52 -0800371 return null;
372 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800373
374 /**
Julien Desprez13138de2019-06-10 16:57:19 -0700375 * Parse a URL query style. Delimited by &, and map values represented by =. Example:
376 * ?key=value&key2=value2
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800377 */
Julien Desprez13138de2019-06-10 16:57:19 -0700378 private Map<String, String> parseQuery(String query) {
379 Map<String, String> values = new HashMap<>();
380 if (query == null) {
381 return values;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800382 }
Julien Desprez13138de2019-06-10 16:57:19 -0700383 for (String maps : query.split("&")) {
384 String[] keyVal = maps.split("=");
385 values.put(keyVal[0], keyVal[1]);
386 }
387 return values;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800388 }
Julien Despreza0a4aa72019-06-18 11:36:22 -0700389
390 /** Whether or not a link was requested as optional. */
391 private boolean isOptional(Map<String, String> query) {
392 String value = query.get(OPTIONAL_KEY);
393 if (value == null) {
394 return false;
395 }
396 return "true".equals(value.toLowerCase());
397 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800398}