blob: be98742afac0124aaefbdd4408934b86de6680c8 [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;
21import com.android.tradefed.config.remote.IRemoteFileResolver;
Julien Desprez6f8cb8d2019-06-05 10:57:48 -070022import com.android.tradefed.config.remote.LocalFileResolver;
Julien Desprezd65e6912018-12-13 15:20:52 -080023import com.android.tradefed.log.LogUtil.CLog;
24import com.android.tradefed.util.FileUtil;
Julien Despreze30f0d92019-01-07 17:32:14 -080025import com.android.tradefed.util.MultiMap;
Julien Desprezc18493d2019-06-13 09:49:29 -070026import com.android.tradefed.util.ZipUtil;
27import com.android.tradefed.util.ZipUtil2;
Julien Desprezd65e6912018-12-13 15:20:52 -080028
29import java.io.File;
Julien Desprezc18493d2019-06-13 09:49:29 -070030import java.io.IOException;
Julien Desprezd65e6912018-12-13 15:20:52 -080031import java.lang.reflect.Field;
Julien Desprez13138de2019-06-10 16:57:19 -070032import java.net.URI;
33import java.net.URISyntaxException;
Julien Desprezd65e6912018-12-13 15:20:52 -080034import java.util.ArrayList;
35import java.util.Collection;
Julien Desprez27bcd4b2018-12-19 12:49:08 -080036import java.util.HashMap;
Julien Desprezd65e6912018-12-13 15:20:52 -080037import java.util.HashSet;
Julien Desprez8f0432f2019-01-04 13:47:54 -080038import java.util.LinkedHashMap;
Julien Despreze30f0d92019-01-07 17:32:14 -080039import java.util.List;
Julien Desprezd65e6912018-12-13 15:20:52 -080040import java.util.Map;
Julien Desprez8f0432f2019-01-04 13:47:54 -080041import java.util.Map.Entry;
Julien Desprezd65e6912018-12-13 15:20:52 -080042import java.util.Set;
Julien Desprez15d9c7d2019-01-02 11:15:18 -080043import java.util.concurrent.atomic.AtomicBoolean;
Julien Desprezd65e6912018-12-13 15:20:52 -080044
45/**
46 * Class that helps resolving path to remote files.
47 *
48 * <p>For example: gs://bucket/path/file.txt will be resolved by downloading the file from the GCS
49 * bucket.
Julien Desprezd65e6912018-12-13 15:20:52 -080050 */
51public class DynamicRemoteFileResolver {
52
Julien Desprez15d9c7d2019-01-02 11:15:18 -080053 public static final String DYNAMIC_RESOLVER = "dynamic-resolver";
Julien Desprez27bcd4b2018-12-19 12:49:08 -080054 private static final Map<String, IRemoteFileResolver> PROTOCOL_SUPPORT = new HashMap<>();
55
56 static {
Julien Desprez27bcd4b2018-12-19 12:49:08 -080057 PROTOCOL_SUPPORT.put(GcsRemoteFileResolver.PROTOCOL, new GcsRemoteFileResolver());
Julien Desprez6f8cb8d2019-06-05 10:57:48 -070058 PROTOCOL_SUPPORT.put(LocalFileResolver.PROTOCOL, new LocalFileResolver());
Julien Desprez27bcd4b2018-12-19 12:49:08 -080059 }
Julien Desprez15d9c7d2019-01-02 11:15:18 -080060 // The configuration map being static, we only need to update it once per TF instance.
61 private static AtomicBoolean sIsUpdateDone = new AtomicBoolean(false);
Julien Desprezc18493d2019-06-13 09:49:29 -070062 // Query key for requesting to unzip a downloaded file automatically.
63 public static final String UNZIP_KEY = "unzip";
Julien Despreza0a4aa72019-06-18 11:36:22 -070064 // Query key for requesting a download to be optional, so if it fails we don't replace it.
65 public static final String OPTIONAL_KEY = "optional";
Julien Desprez27bcd4b2018-12-19 12:49:08 -080066
Julien Desprezd65e6912018-12-13 15:20:52 -080067 private Map<String, OptionFieldsForName> mOptionMap;
68
69 /** Sets the map of options coming from {@link OptionSetter} */
70 public void setOptionMap(Map<String, OptionFieldsForName> optionMap) {
71 mOptionMap = optionMap;
72 }
73
74 /**
75 * Runs through all the {@link File} option type and check if their path should be resolved.
76 *
77 * @return The list of {@link File} that was resolved that way.
78 * @throws ConfigurationException
79 */
80 public final Set<File> validateRemoteFilePath() throws ConfigurationException {
81 Set<File> downloadedFiles = new HashSet<>();
82 try {
Julien Despreza0a4aa72019-06-18 11:36:22 -070083 Set<Field> fieldSet = new HashSet<>();
Julien Desprezd65e6912018-12-13 15:20:52 -080084 for (Map.Entry<String, OptionFieldsForName> optionPair : mOptionMap.entrySet()) {
Julien Desprezd65e6912018-12-13 15:20:52 -080085 final OptionFieldsForName optionFields = optionPair.getValue();
Julien Desprezd65e6912018-12-13 15:20:52 -080086 for (Map.Entry<Object, Field> fieldEntry : optionFields) {
Julien Despreza0a4aa72019-06-18 11:36:22 -070087
Julien Desprezd65e6912018-12-13 15:20:52 -080088 final Object obj = fieldEntry.getKey();
Julien Despreza0a4aa72019-06-18 11:36:22 -070089
Julien Desprezd65e6912018-12-13 15:20:52 -080090 final Field field = fieldEntry.getValue();
Julien Despreza0a4aa72019-06-18 11:36:22 -070091 if (fieldSet.contains(field)) {
92 // Avoid reprocessing a Field we already saw.
93 continue;
94 }
95 fieldSet.add(field);
Julien Desprezd65e6912018-12-13 15:20:52 -080096 final Option option = field.getAnnotation(Option.class);
97 if (option == null) {
98 continue;
99 }
100 // At this point, we know this is an option field; make sure it's set
101 field.setAccessible(true);
102 final Object value;
103 try {
104 value = field.get(obj);
105 } catch (IllegalAccessException e) {
106 throw new ConfigurationException(
107 String.format("internal error: %s", e.getMessage()));
108 }
109
110 if (value == null) {
111 continue;
112 } else if (value instanceof File) {
113 File consideredFile = (File) value;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800114 File downloadedFile = resolveRemoteFiles(consideredFile, option);
Julien Desprezd65e6912018-12-13 15:20:52 -0800115 if (downloadedFile != null) {
116 downloadedFiles.add(downloadedFile);
117 // Replace the field value
118 try {
119 field.set(obj, downloadedFile);
120 } catch (IllegalAccessException e) {
121 CLog.e(e);
122 throw new ConfigurationException(
123 String.format(
Julien Desprez65249662018-12-27 16:50:29 -0800124 "Failed to download %s due to '%s'",
125 consideredFile.getPath(), e.getMessage()),
Julien Desprezd65e6912018-12-13 15:20:52 -0800126 e);
127 }
128 }
129 } else if (value instanceof Collection) {
130 Collection<Object> c = (Collection<Object>) value;
131 Collection<Object> copy = new ArrayList<>(c);
132 for (Object o : copy) {
133 if (o instanceof File) {
134 File consideredFile = (File) o;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800135 File downloadedFile = resolveRemoteFiles(consideredFile, option);
Julien Desprezd65e6912018-12-13 15:20:52 -0800136 if (downloadedFile != null) {
137 downloadedFiles.add(downloadedFile);
138 // TODO: See if order could be preserved.
139 c.remove(consideredFile);
140 c.add(downloadedFile);
141 }
142 }
143 }
Julien Desprez8f0432f2019-01-04 13:47:54 -0800144 } else if (value instanceof Map) {
145 Map<Object, Object> m = (Map<Object, Object>) value;
146 Map<Object, Object> copy = new LinkedHashMap<>(m);
147 for (Entry<Object, Object> entry : copy.entrySet()) {
148 Object key = entry.getKey();
149 Object val = entry.getValue();
150
151 Object finalKey = key;
152 Object finalVal = val;
153 if (key instanceof File) {
154 key = resolveRemoteFiles((File) key, option);
155 if (key != null) {
156 downloadedFiles.add((File) key);
157 finalKey = key;
158 }
159 }
160 if (val instanceof File) {
161 val = resolveRemoteFiles((File) val, option);
162 if (val != null) {
163 downloadedFiles.add((File) val);
164 finalVal = val;
165 }
166 }
167
168 m.remove(entry.getKey());
169 m.put(finalKey, finalVal);
170 }
Julien Despreze30f0d92019-01-07 17:32:14 -0800171 } else if (value instanceof MultiMap) {
172 MultiMap<Object, Object> m = (MultiMap<Object, Object>) value;
173 MultiMap<Object, Object> copy = new MultiMap<>(m);
174 for (Object key : copy.keySet()) {
175 List<Object> mapValues = copy.get(key);
176
177 m.remove(key);
178 Object finalKey = key;
179 if (key instanceof File) {
180 key = resolveRemoteFiles((File) key, option);
181 if (key != null) {
182 downloadedFiles.add((File) key);
183 finalKey = key;
184 }
185 }
186 for (Object mapValue : mapValues) {
187 if (mapValue instanceof File) {
188 File f = resolveRemoteFiles((File) mapValue, option);
189 if (f != null) {
190 downloadedFiles.add(f);
191 mapValue = f;
192 }
193 }
194 m.put(finalKey, mapValue);
195 }
196 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800197 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800198 }
199 }
200 } catch (ConfigurationException e) {
201 // Clean up the files before throwing
202 for (File f : downloadedFiles) {
203 FileUtil.recursiveDelete(f);
204 }
205 throw e;
206 }
207 return downloadedFiles;
208 }
209
Dan Shif663f592019-06-25 16:17:16 -0700210 /**
211 * Download the files matching given filters in a remote zip file.
212 *
213 * <p>A file inside the remote zip file is only downloaded if its path matches any of the
214 * include filters but not the exclude filters.
215 *
216 * @param destDir the file to place the downloaded contents into.
217 * @param remoteZipFilePath the remote path to the zip file to download, relative to an
218 * implementation specific root.
219 * @param includeFilters a list of regex strings to download matching files. A file's path
220 * matching any filter will be downloaded.
221 * @param excludeFilters a list of regex strings to skip downloading matching files. A file's
222 * path matching any filter will not be downloaded.
223 * @throws ConfigurationException if files could not be downloaded.
224 */
225 public void resolvePartialDownloadZip(
226 File destDir,
227 String remoteZipFilePath,
228 List<String> includeFilters,
229 List<String> excludeFilters)
230 throws ConfigurationException {
231 Map<String, String> queryArgs;
232 String protocol;
233 try {
234 URI uri = new URI(remoteZipFilePath);
235 protocol = uri.getScheme();
236 queryArgs = parseQuery(uri.getQuery());
237 } catch (URISyntaxException e) {
238 throw new ConfigurationException(
239 String.format(
240 "Failed to parse the remote zip file path: %s", remoteZipFilePath),
241 e);
242 }
243 IRemoteFileResolver resolver = getResolver(protocol);
244
245 queryArgs.put("partial_download_dir", destDir.getAbsolutePath());
246 if (includeFilters != null) {
247 queryArgs.put("include_filters", String.join(";", includeFilters));
248 }
249 if (excludeFilters != null) {
250 queryArgs.put("exclude_filters", String.join(";", excludeFilters));
251 }
252 // Downloaded individual files should be saved to destDir, return value is not needed.
253 try {
254 resolver.resolveRemoteFiles(new File(remoteZipFilePath), null, queryArgs);
255 } catch (ConfigurationException e) {
256 if (isOptional(queryArgs)) {
257 CLog.d(
258 "Failed to partially download '%s' but marked optional so skipping: %s",
259 remoteZipFilePath, e.getMessage());
260 } else {
261 throw e;
262 }
263 }
264 }
265
Julien Desprezd65e6912018-12-13 15:20:52 -0800266 @VisibleForTesting
Julien Desprez9b477232019-03-07 11:08:25 -0800267 protected IRemoteFileResolver getResolver(String protocol) {
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800268 if (updateProtocols()) {
269 IGlobalConfiguration globalConfig = getGlobalConfig();
270 Object o = globalConfig.getConfigurationObject(DYNAMIC_RESOLVER);
271 if (o != null) {
272 if (o instanceof IRemoteFileResolver) {
273 IRemoteFileResolver resolver = (IRemoteFileResolver) o;
274 CLog.d("Adding %s to supported remote file resolver", resolver);
275 PROTOCOL_SUPPORT.put(resolver.getSupportedProtocol(), resolver);
276 } else {
277 CLog.e("%s is not of type IRemoteFileResolver", o);
278 }
279 }
280 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800281 return PROTOCOL_SUPPORT.get(protocol);
Julien Desprezd65e6912018-12-13 15:20:52 -0800282 }
283
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800284 @VisibleForTesting
Julien Desprez9b477232019-03-07 11:08:25 -0800285 protected boolean updateProtocols() {
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800286 return sIsUpdateDone.compareAndSet(false, true);
287 }
288
289 @VisibleForTesting
290 IGlobalConfiguration getGlobalConfig() {
291 return GlobalConfiguration.getInstance();
292 }
293
Julien Desprezc18493d2019-06-13 09:49:29 -0700294 /**
295 * Utility that allows to check whether or not a file should be unzip and unzip it if required.
296 */
297 public static final File unzipIfRequired(File downloadedFile, Map<String, String> query)
298 throws IOException {
299 String unzipValue = query.get(UNZIP_KEY);
300 if (unzipValue != null && "true".equals(unzipValue.toLowerCase())) {
301 // File was requested to be unzipped.
302 if (ZipUtil.isZipFileValid(downloadedFile, false)) {
303 File unzipped =
304 ZipUtil2.extractZipToTemp(
305 downloadedFile, FileUtil.getBaseName(downloadedFile.getName()));
306 FileUtil.deleteFile(downloadedFile);
307 return unzipped;
308 } else {
309 CLog.w("%s was requested to be unzipped but is not a valid zip.", downloadedFile);
310 }
311 }
312 // Return the original file untouched
313 return downloadedFile;
314 }
315
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800316 private File resolveRemoteFiles(File consideredFile, Option option)
Julien Desprezd65e6912018-12-13 15:20:52 -0800317 throws ConfigurationException {
Julien Desprez13138de2019-06-10 16:57:19 -0700318 File fileToResolve;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800319 String path = consideredFile.getPath();
Julien Desprez13138de2019-06-10 16:57:19 -0700320 String protocol;
321 Map<String, String> query;
322 try {
323 URI uri = new URI(path);
324 protocol = uri.getScheme();
325 query = parseQuery(uri.getQuery());
326 fileToResolve = new File(protocol + ":" + uri.getPath());
327 } catch (URISyntaxException e) {
328 CLog.e(e);
329 return null;
330 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800331 IRemoteFileResolver resolver = getResolver(protocol);
332 if (resolver != null) {
Julien Despreza0a4aa72019-06-18 11:36:22 -0700333 try {
334 return resolver.resolveRemoteFiles(fileToResolve, option, query);
335 } catch (ConfigurationException e) {
336 if (isOptional(query)) {
337 CLog.d(
338 "Failed to resolve '%s' but marked optional so skipping: %s",
339 fileToResolve, e.getMessage());
340 } else {
341 throw e;
342 }
343 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800344 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800345 // Not a remote file
Julien Desprezd65e6912018-12-13 15:20:52 -0800346 return null;
347 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800348
349 /**
Julien Desprez13138de2019-06-10 16:57:19 -0700350 * Parse a URL query style. Delimited by &, and map values represented by =. Example:
351 * ?key=value&key2=value2
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800352 */
Julien Desprez13138de2019-06-10 16:57:19 -0700353 private Map<String, String> parseQuery(String query) {
354 Map<String, String> values = new HashMap<>();
355 if (query == null) {
356 return values;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800357 }
Julien Desprez13138de2019-06-10 16:57:19 -0700358 for (String maps : query.split("&")) {
359 String[] keyVal = maps.split("=");
360 values.put(keyVal[0], keyVal[1]);
361 }
362 return values;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800363 }
Julien Despreza0a4aa72019-06-18 11:36:22 -0700364
365 /** Whether or not a link was requested as optional. */
366 private boolean isOptional(Map<String, String> query) {
367 String value = query.get(OPTIONAL_KEY);
368 if (value == null) {
369 return false;
370 }
371 return "true".equals(value.toLowerCase());
372 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800373}