blob: f3f1561e988a8f0ac494e8ebf66279083c4e6c0f [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 Desprezd65e6912018-12-13 15:20:52 -080019import com.android.tradefed.config.OptionSetter.OptionFieldsForName;
Julien Desprez27bcd4b2018-12-19 12:49:08 -080020import com.android.tradefed.config.remote.GcsRemoteFileResolver;
Julien Desprezfb7b4f62019-07-03 14:05:29 -070021import com.android.tradefed.config.remote.HttpRemoteFileResolver;
22import com.android.tradefed.config.remote.HttpsRemoteFileResolver;
Julien Desprez27bcd4b2018-12-19 12:49:08 -080023import com.android.tradefed.config.remote.IRemoteFileResolver;
Julien Desprez6f8cb8d2019-06-05 10:57:48 -070024import com.android.tradefed.config.remote.LocalFileResolver;
Julien Desprezd65e6912018-12-13 15:20:52 -080025import com.android.tradefed.log.LogUtil.CLog;
26import com.android.tradefed.util.FileUtil;
Julien Despreze30f0d92019-01-07 17:32:14 -080027import com.android.tradefed.util.MultiMap;
Julien Desprezc18493d2019-06-13 09:49:29 -070028import com.android.tradefed.util.ZipUtil;
29import com.android.tradefed.util.ZipUtil2;
Julien Desprezd65e6912018-12-13 15:20:52 -080030
31import java.io.File;
Julien Desprezc18493d2019-06-13 09:49:29 -070032import java.io.IOException;
Julien Desprezd65e6912018-12-13 15:20:52 -080033import java.lang.reflect.Field;
Julien Desprez13138de2019-06-10 16:57:19 -070034import java.net.URI;
35import java.net.URISyntaxException;
Julien Desprezd65e6912018-12-13 15:20:52 -080036import java.util.ArrayList;
37import java.util.Collection;
Julien Desprez27bcd4b2018-12-19 12:49:08 -080038import java.util.HashMap;
Julien Desprezd65e6912018-12-13 15:20:52 -080039import java.util.HashSet;
Julien Desprez8f0432f2019-01-04 13:47:54 -080040import java.util.LinkedHashMap;
Julien Despreze30f0d92019-01-07 17:32:14 -080041import java.util.List;
Julien Desprezd65e6912018-12-13 15:20:52 -080042import java.util.Map;
Julien Desprez8f0432f2019-01-04 13:47:54 -080043import java.util.Map.Entry;
Julien Desprezd65e6912018-12-13 15:20:52 -080044import java.util.Set;
Julien Desprez15d9c7d2019-01-02 11:15:18 -080045import java.util.concurrent.atomic.AtomicBoolean;
Julien Desprezd65e6912018-12-13 15:20:52 -080046
47/**
48 * Class that helps resolving path to remote files.
49 *
50 * <p>For example: gs://bucket/path/file.txt will be resolved by downloading the file from the GCS
51 * bucket.
Julien Desprezd65e6912018-12-13 15:20:52 -080052 */
53public class DynamicRemoteFileResolver {
54
Julien Desprez15d9c7d2019-01-02 11:15:18 -080055 public static final String DYNAMIC_RESOLVER = "dynamic-resolver";
Julien Desprez27bcd4b2018-12-19 12:49:08 -080056 private static final Map<String, IRemoteFileResolver> PROTOCOL_SUPPORT = new HashMap<>();
57
58 static {
Julien Desprez27bcd4b2018-12-19 12:49:08 -080059 PROTOCOL_SUPPORT.put(GcsRemoteFileResolver.PROTOCOL, new GcsRemoteFileResolver());
Julien Desprez6f8cb8d2019-06-05 10:57:48 -070060 PROTOCOL_SUPPORT.put(LocalFileResolver.PROTOCOL, new LocalFileResolver());
Julien Desprezfb7b4f62019-07-03 14:05:29 -070061 PROTOCOL_SUPPORT.put(HttpRemoteFileResolver.PROTOCOL_HTTP, new HttpRemoteFileResolver());
62 PROTOCOL_SUPPORT.put(HttpsRemoteFileResolver.PROTOCOL_HTTPS, new HttpsRemoteFileResolver());
Julien Desprez27bcd4b2018-12-19 12:49:08 -080063 }
Julien Desprez15d9c7d2019-01-02 11:15:18 -080064 // The configuration map being static, we only need to update it once per TF instance.
65 private static AtomicBoolean sIsUpdateDone = new AtomicBoolean(false);
Julien Desprezc18493d2019-06-13 09:49:29 -070066 // Query key for requesting to unzip a downloaded file automatically.
67 public static final String UNZIP_KEY = "unzip";
Julien Despreza0a4aa72019-06-18 11:36:22 -070068 // Query key for requesting a download to be optional, so if it fails we don't replace it.
69 public static final String OPTIONAL_KEY = "optional";
Julien Desprez27bcd4b2018-12-19 12:49:08 -080070
Julien Desprezd65e6912018-12-13 15:20:52 -080071 private Map<String, OptionFieldsForName> mOptionMap;
72
73 /** Sets the map of options coming from {@link OptionSetter} */
74 public void setOptionMap(Map<String, OptionFieldsForName> optionMap) {
75 mOptionMap = optionMap;
76 }
77
78 /**
79 * Runs through all the {@link File} option type and check if their path should be resolved.
80 *
81 * @return The list of {@link File} that was resolved that way.
82 * @throws ConfigurationException
83 */
84 public final Set<File> validateRemoteFilePath() throws ConfigurationException {
85 Set<File> downloadedFiles = new HashSet<>();
86 try {
Julien Desprezcf0f0b22019-09-18 14:34:28 -070087 Map<Field, Object> fieldSeen = new HashMap<>();
Julien Desprezd65e6912018-12-13 15:20:52 -080088 for (Map.Entry<String, OptionFieldsForName> optionPair : mOptionMap.entrySet()) {
Julien Desprezd65e6912018-12-13 15:20:52 -080089 final OptionFieldsForName optionFields = optionPair.getValue();
Julien Desprezd65e6912018-12-13 15:20:52 -080090 for (Map.Entry<Object, Field> fieldEntry : optionFields) {
Julien Despreza0a4aa72019-06-18 11:36:22 -070091
Julien Desprezd65e6912018-12-13 15:20:52 -080092 final Object obj = fieldEntry.getKey();
93 final Field field = fieldEntry.getValue();
94 final Option option = field.getAnnotation(Option.class);
95 if (option == null) {
96 continue;
97 }
98 // At this point, we know this is an option field; make sure it's set
99 field.setAccessible(true);
100 final Object value;
101 try {
102 value = field.get(obj);
Julien Desprezc0541882019-07-10 08:36:02 -0700103 if (value == null) {
104 continue;
105 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800106 } catch (IllegalAccessException e) {
107 throw new ConfigurationException(
108 String.format("internal error: %s", e.getMessage()));
109 }
110
Julien Desprezcf0f0b22019-09-18 14:34:28 -0700111 if (fieldSeen.get(field) != null && fieldSeen.get(field).equals(obj)) {
Julien Desprezd65e6912018-12-13 15:20:52 -0800112 continue;
Julien Desprezc0541882019-07-10 08:36:02 -0700113 }
114 // Keep track of the field set on each object
Julien Desprezcf0f0b22019-09-18 14:34:28 -0700115 fieldSeen.put(field, obj);
Julien Desprezc0541882019-07-10 08:36:02 -0700116
117 if (value instanceof File) {
Julien Desprezd65e6912018-12-13 15:20:52 -0800118 File consideredFile = (File) value;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800119 File downloadedFile = resolveRemoteFiles(consideredFile, option);
Julien Desprezd65e6912018-12-13 15:20:52 -0800120 if (downloadedFile != null) {
121 downloadedFiles.add(downloadedFile);
122 // Replace the field value
123 try {
124 field.set(obj, downloadedFile);
125 } catch (IllegalAccessException e) {
126 CLog.e(e);
127 throw new ConfigurationException(
128 String.format(
Julien Desprez65249662018-12-27 16:50:29 -0800129 "Failed to download %s due to '%s'",
130 consideredFile.getPath(), e.getMessage()),
Julien Desprezd65e6912018-12-13 15:20:52 -0800131 e);
132 }
133 }
134 } else if (value instanceof Collection) {
135 Collection<Object> c = (Collection<Object>) value;
136 Collection<Object> copy = new ArrayList<>(c);
137 for (Object o : copy) {
138 if (o instanceof File) {
139 File consideredFile = (File) o;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800140 File downloadedFile = resolveRemoteFiles(consideredFile, option);
Julien Desprezd65e6912018-12-13 15:20:52 -0800141 if (downloadedFile != null) {
142 downloadedFiles.add(downloadedFile);
143 // TODO: See if order could be preserved.
144 c.remove(consideredFile);
145 c.add(downloadedFile);
146 }
147 }
148 }
Julien Desprez8f0432f2019-01-04 13:47:54 -0800149 } else if (value instanceof Map) {
150 Map<Object, Object> m = (Map<Object, Object>) value;
151 Map<Object, Object> copy = new LinkedHashMap<>(m);
152 for (Entry<Object, Object> entry : copy.entrySet()) {
153 Object key = entry.getKey();
154 Object val = entry.getValue();
155
156 Object finalKey = key;
157 Object finalVal = val;
158 if (key instanceof File) {
159 key = resolveRemoteFiles((File) key, option);
160 if (key != null) {
161 downloadedFiles.add((File) key);
162 finalKey = key;
163 }
164 }
165 if (val instanceof File) {
166 val = resolveRemoteFiles((File) val, option);
167 if (val != null) {
168 downloadedFiles.add((File) val);
169 finalVal = val;
170 }
171 }
172
173 m.remove(entry.getKey());
174 m.put(finalKey, finalVal);
175 }
Julien Despreze30f0d92019-01-07 17:32:14 -0800176 } else if (value instanceof MultiMap) {
177 MultiMap<Object, Object> m = (MultiMap<Object, Object>) value;
178 MultiMap<Object, Object> copy = new MultiMap<>(m);
179 for (Object key : copy.keySet()) {
180 List<Object> mapValues = copy.get(key);
181
182 m.remove(key);
183 Object finalKey = key;
184 if (key instanceof File) {
185 key = resolveRemoteFiles((File) key, option);
186 if (key != null) {
187 downloadedFiles.add((File) key);
188 finalKey = key;
189 }
190 }
191 for (Object mapValue : mapValues) {
192 if (mapValue instanceof File) {
193 File f = resolveRemoteFiles((File) mapValue, option);
194 if (f != null) {
195 downloadedFiles.add(f);
196 mapValue = f;
197 }
198 }
199 m.put(finalKey, mapValue);
200 }
201 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800202 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800203 }
204 }
205 } catch (ConfigurationException e) {
206 // Clean up the files before throwing
207 for (File f : downloadedFiles) {
208 FileUtil.recursiveDelete(f);
209 }
210 throw e;
211 }
212 return downloadedFiles;
213 }
214
Dan Shif663f592019-06-25 16:17:16 -0700215 /**
216 * Download the files matching given filters in a remote zip file.
217 *
218 * <p>A file inside the remote zip file is only downloaded if its path matches any of the
219 * include filters but not the exclude filters.
220 *
221 * @param destDir the file to place the downloaded contents into.
222 * @param remoteZipFilePath the remote path to the zip file to download, relative to an
223 * implementation specific root.
224 * @param includeFilters a list of regex strings to download matching files. A file's path
225 * matching any filter will be downloaded.
226 * @param excludeFilters a list of regex strings to skip downloading matching files. A file's
227 * path matching any filter will not be downloaded.
228 * @throws ConfigurationException if files could not be downloaded.
229 */
230 public void resolvePartialDownloadZip(
231 File destDir,
232 String remoteZipFilePath,
233 List<String> includeFilters,
234 List<String> excludeFilters)
235 throws ConfigurationException {
236 Map<String, String> queryArgs;
237 String protocol;
238 try {
239 URI uri = new URI(remoteZipFilePath);
240 protocol = uri.getScheme();
241 queryArgs = parseQuery(uri.getQuery());
242 } catch (URISyntaxException e) {
243 throw new ConfigurationException(
244 String.format(
245 "Failed to parse the remote zip file path: %s", remoteZipFilePath),
246 e);
247 }
248 IRemoteFileResolver resolver = getResolver(protocol);
249
250 queryArgs.put("partial_download_dir", destDir.getAbsolutePath());
251 if (includeFilters != null) {
252 queryArgs.put("include_filters", String.join(";", includeFilters));
253 }
254 if (excludeFilters != null) {
255 queryArgs.put("exclude_filters", String.join(";", excludeFilters));
256 }
257 // Downloaded individual files should be saved to destDir, return value is not needed.
258 try {
259 resolver.resolveRemoteFiles(new File(remoteZipFilePath), null, queryArgs);
260 } catch (ConfigurationException e) {
261 if (isOptional(queryArgs)) {
262 CLog.d(
263 "Failed to partially download '%s' but marked optional so skipping: %s",
264 remoteZipFilePath, e.getMessage());
265 } else {
266 throw e;
267 }
268 }
269 }
270
Julien Desprezd65e6912018-12-13 15:20:52 -0800271 @VisibleForTesting
Julien Desprez9b477232019-03-07 11:08:25 -0800272 protected IRemoteFileResolver getResolver(String protocol) {
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800273 if (updateProtocols()) {
274 IGlobalConfiguration globalConfig = getGlobalConfig();
275 Object o = globalConfig.getConfigurationObject(DYNAMIC_RESOLVER);
276 if (o != null) {
277 if (o instanceof IRemoteFileResolver) {
278 IRemoteFileResolver resolver = (IRemoteFileResolver) o;
279 CLog.d("Adding %s to supported remote file resolver", resolver);
280 PROTOCOL_SUPPORT.put(resolver.getSupportedProtocol(), resolver);
281 } else {
282 CLog.e("%s is not of type IRemoteFileResolver", o);
283 }
284 }
285 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800286 return PROTOCOL_SUPPORT.get(protocol);
Julien Desprezd65e6912018-12-13 15:20:52 -0800287 }
288
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800289 @VisibleForTesting
Julien Desprez9b477232019-03-07 11:08:25 -0800290 protected boolean updateProtocols() {
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800291 return sIsUpdateDone.compareAndSet(false, true);
292 }
293
294 @VisibleForTesting
295 IGlobalConfiguration getGlobalConfig() {
296 return GlobalConfiguration.getInstance();
297 }
298
Julien Desprezc18493d2019-06-13 09:49:29 -0700299 /**
300 * Utility that allows to check whether or not a file should be unzip and unzip it if required.
301 */
302 public static final File unzipIfRequired(File downloadedFile, Map<String, String> query)
303 throws IOException {
304 String unzipValue = query.get(UNZIP_KEY);
305 if (unzipValue != null && "true".equals(unzipValue.toLowerCase())) {
306 // File was requested to be unzipped.
307 if (ZipUtil.isZipFileValid(downloadedFile, false)) {
308 File unzipped =
309 ZipUtil2.extractZipToTemp(
310 downloadedFile, FileUtil.getBaseName(downloadedFile.getName()));
311 FileUtil.deleteFile(downloadedFile);
312 return unzipped;
313 } else {
314 CLog.w("%s was requested to be unzipped but is not a valid zip.", downloadedFile);
315 }
316 }
317 // Return the original file untouched
318 return downloadedFile;
319 }
320
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800321 private File resolveRemoteFiles(File consideredFile, Option option)
Julien Desprezd65e6912018-12-13 15:20:52 -0800322 throws ConfigurationException {
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 {
339 return resolver.resolveRemoteFiles(fileToResolve, option, query);
340 } catch (ConfigurationException e) {
341 if (isOptional(query)) {
342 CLog.d(
343 "Failed to resolve '%s' but marked optional so skipping: %s",
344 fileToResolve, e.getMessage());
345 } else {
346 throw e;
347 }
348 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800349 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800350 // Not a remote file
Julien Desprezd65e6912018-12-13 15:20:52 -0800351 return null;
352 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800353
354 /**
Julien Desprez13138de2019-06-10 16:57:19 -0700355 * Parse a URL query style. Delimited by &, and map values represented by =. Example:
356 * ?key=value&key2=value2
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800357 */
Julien Desprez13138de2019-06-10 16:57:19 -0700358 private Map<String, String> parseQuery(String query) {
359 Map<String, String> values = new HashMap<>();
360 if (query == null) {
361 return values;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800362 }
Julien Desprez13138de2019-06-10 16:57:19 -0700363 for (String maps : query.split("&")) {
364 String[] keyVal = maps.split("=");
365 values.put(keyVal[0], keyVal[1]);
366 }
367 return values;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800368 }
Julien Despreza0a4aa72019-06-18 11:36:22 -0700369
370 /** Whether or not a link was requested as optional. */
371 private boolean isOptional(Map<String, String> query) {
372 String value = query.get(OPTIONAL_KEY);
373 if (value == null) {
374 return false;
375 }
376 return "true".equals(value.toLowerCase());
377 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800378}