View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.buildcache.its;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.nio.charset.StandardCharsets;
24  import java.nio.file.Files;
25  import java.nio.file.LinkOption;
26  import java.nio.file.Path;
27  import java.nio.file.Paths;
28  import java.nio.file.attribute.FileTime;
29  import java.nio.file.attribute.PosixFilePermission;
30  import java.nio.file.attribute.PosixFilePermissions;
31  import java.time.Instant;
32  import java.time.ZoneId;
33  import java.time.ZonedDateTime;
34  import java.time.format.DateTimeFormatter;
35  import java.util.Arrays;
36  import java.util.EnumSet;
37  import java.util.Map;
38  import java.util.Objects;
39  import java.util.Set;
40  import java.util.TreeMap;
41  import java.util.function.Consumer;
42  import java.util.function.Predicate;
43  import java.util.regex.Pattern;
44  import java.util.stream.Collectors;
45  import java.util.stream.Stream;
46  
47  import org.apache.maven.buildcache.its.junit.BeforeEach;
48  import org.apache.maven.buildcache.its.junit.Inject;
49  import org.apache.maven.buildcache.its.junit.IntegrationTest;
50  import org.apache.maven.buildcache.its.junit.IntegrationTestExtension;
51  import org.apache.maven.it.VerificationException;
52  import org.apache.maven.it.Verifier;
53  import org.jetbrains.annotations.NotNull;
54  import org.junit.jupiter.api.AfterEach;
55  import org.junit.jupiter.params.ParameterizedTest;
56  import org.junit.jupiter.params.provider.Arguments;
57  import org.junit.jupiter.params.provider.MethodSource;
58  import org.testcontainers.containers.GenericContainer;
59  import org.testcontainers.junit.jupiter.Container;
60  import org.testcontainers.junit.jupiter.Testcontainers;
61  import org.testcontainers.utility.DockerImageName;
62  
63  import static org.apache.maven.buildcache.xml.CacheConfigImpl.REMOTE_SERVER_ID_PROPERTY_NAME;
64  import static org.apache.maven.buildcache.xml.CacheConfigImpl.REMOTE_URL_PROPERTY_NAME;
65  import static org.junit.jupiter.api.Assertions.assertFalse;
66  import static org.junit.jupiter.api.Assertions.assertTrue;
67  
68  @IntegrationTest("src/test/projects/remote-cache-dav")
69  @Testcontainers(disabledWithoutDocker = true)
70  public class RemoteCacheDavTest {
71  
72      public static final String DAV_DOCKER_IMAGE =
73              "xama/nginx-webdav@sha256:84171a7e67d7e98eeaa67de58e3ce141ec1d0ee9c37004e7096698c8379fd9cf";
74      private static final String DAV_USERNAME = "admin";
75      private static final String DAV_PASSWORD = "admin";
76      private static final String REPO_ID = "build-cache";
77      private static final String HTTP_TRANSPORT_PRIORITY =
78              "aether.priority.org.eclipse.aether.transport.http.HttpTransporterFactory";
79      private static final String WAGON_TRANSPORT_PRIORITY =
80              "aether.priority.org.eclipse.aether.transport.wagon.WagonTransporterFactory";
81      private static final String MAVEN_BUILD_CACHE_REMOTE_SAVE_ENABLED = "maven.build.cache.remote.save.enabled";
82  
83      @Container
84      GenericContainer<?> dav;
85  
86      @Inject
87      Verifier verifier;
88  
89      Path basedir;
90      Path remoteCache;
91      Path localCache;
92      Path settings;
93      Path logDir;
94  
95      @BeforeEach
96      void setup() throws IOException {
97          basedir = Paths.get(verifier.getBasedir());
98          remoteCache = basedir.resolveSibling("cache-remote").toAbsolutePath().normalize();
99          localCache = basedir.resolveSibling("cache-local").toAbsolutePath().normalize();
100         settings = basedir.resolve("../settings.xml").toAbsolutePath().normalize();
101         logDir = basedir.getParent();
102 
103         Files.createDirectories(remoteCache);
104 
105         Files.write(
106                 settings,
107                 ("<settings>"
108                                 + "<servers><server>"
109                                 + "<id>" + REPO_ID + "</id>"
110                                 + "<username>" + DAV_USERNAME + "</username>"
111                                 + "<password>" + DAV_PASSWORD + "</password>"
112                                 + "</server></servers></settings>")
113                         .getBytes());
114 
115         dav = new GenericContainer<>(DockerImageName.parse(DAV_DOCKER_IMAGE))
116                 .withReuse(false)
117                 .withExposedPorts(80)
118                 .withEnv("WEBDAV_USERNAME", DAV_USERNAME)
119                 .withEnv("WEBDAV_PASSWORD", DAV_PASSWORD)
120                 .withFileSystemBind(remoteCache.toString(), "/var/webdav/public");
121     }
122 
123     @AfterEach
124     public void cleanup() throws Exception {
125         dav.execInContainer("rm", "-rf", "/var/webdav");
126         cleanDirs(localCache);
127         dav.close();
128     }
129 
130     public static Stream<Arguments> transports() {
131         return Stream.of(Arguments.of("wagon"), Arguments.of("http"));
132     }
133 
134     @ParameterizedTest
135     @MethodSource("transports")
136     public void doTestRemoteCache(String transport) throws VerificationException, IOException {
137         String url =
138                 ("wagon".equals(transport) ? "dav:" : "") + "http://localhost:" + dav.getFirstMappedPort() + "/mbce";
139         substitute(
140                 basedir.resolve(".mvn/maven-build-cache-config.xml"),
141                 "url",
142                 url,
143                 "id",
144                 REPO_ID,
145                 "location",
146                 localCache.toString());
147 
148         verifier.setAutoclean(false);
149 
150         cleanDirs(localCache, remoteCache.resolve("mbce"));
151         assertFalse(hasBuildInfoXml(localCache), () -> error(localCache, "local", false));
152         assertFalse(hasBuildInfoXml(remoteCache), () -> error(remoteCache, "remote", false));
153 
154         verifier.getCliOptions().clear();
155         verifier.addCliOption("--settings=" + settings);
156         verifier.addCliOption("-D" + HTTP_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "0" : "10"));
157         verifier.addCliOption("-D" + WAGON_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "10" : "0"));
158         verifier.addCliOption("-D" + MAVEN_BUILD_CACHE_REMOTE_SAVE_ENABLED + "=false");
159         verifier.setLogFileName("../log-1.txt");
160         verifier.executeGoals(Arrays.asList("clean", "install"));
161         verifier.verifyErrorFreeLog();
162 
163         assertTrue(hasBuildInfoXml(localCache), () -> error(localCache, "local", true));
164         assertFalse(hasBuildInfoXml(remoteCache), () -> error(remoteCache, "remote", false));
165 
166         cleanDirs(localCache);
167 
168         verifier.getCliOptions().clear();
169         verifier.addCliOption("--settings=" + settings);
170         if (!"wagon".equals(transport)) {
171             verifier.setSystemProperty("aether.connector.http.supportWebDav", "true");
172         }
173         verifier.addCliOption("-D" + HTTP_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "0" : "10"));
174         verifier.addCliOption("-D" + WAGON_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "10" : "0"));
175         verifier.addCliOption("-D" + MAVEN_BUILD_CACHE_REMOTE_SAVE_ENABLED + "=true");
176         verifier.setLogFileName("../log-2.txt");
177         verifier.executeGoals(Arrays.asList("clean", "install"));
178         verifier.verifyErrorFreeLog();
179 
180         assertTrue(hasBuildInfoXml(localCache), () -> error(localCache, "local", true));
181         assertTrue(hasBuildInfoXml(remoteCache), () -> error(remoteCache, "remote", true));
182 
183         cleanDirs(localCache);
184 
185         verifier.getCliOptions().clear();
186         verifier.addCliOption("--settings=" + settings);
187         if (!"wagon".equals(transport)) {
188             verifier.setSystemProperty("aether.connector.http.supportWebDav", "true");
189         }
190         verifier.addCliOption("-D" + HTTP_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "0" : "10"));
191         verifier.addCliOption("-D" + WAGON_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "10" : "0"));
192         verifier.addCliOption("-D" + MAVEN_BUILD_CACHE_REMOTE_SAVE_ENABLED + "=false");
193         verifier.setLogFileName("../log-3.txt");
194         verifier.executeGoals(Arrays.asList("clean", "install"));
195         verifier.verifyErrorFreeLog();
196 
197         assertTrue(hasBuildInfoXml(localCache), () -> error(localCache, "local", true));
198         assertTrue(hasBuildInfoXml(remoteCache), () -> error(remoteCache, "remote", true));
199 
200         // replace url and server id with a bad one to be sure cli property is used
201         substitute(
202                 basedir.resolve(".mvn/maven-build-cache-config.xml"),
203                 "url",
204                 "http://foo.com",
205                 "id",
206                 "foo",
207                 "location",
208                 localCache.toString());
209 
210         cleanDirs(localCache);
211         try {
212             // depending on uid used for execution but can be different from the one using docker and so different file
213             // permissions..
214             dav.execInContainer("rm", "-rf", "/var/webdav/public/*");
215         } catch (InterruptedException e) {
216             throw new IOException("cannot delete remote cache");
217         }
218 
219         verifier.getCliOptions().clear();
220         verifier.addCliOption("--settings=" + settings);
221         verifier.addCliOption("-X");
222         verifier.addCliOption("-D" + HTTP_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "0" : "10"));
223         verifier.addCliOption("-D" + WAGON_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "10" : "0"));
224         verifier.addCliOption("-D" + MAVEN_BUILD_CACHE_REMOTE_SAVE_ENABLED + "=true");
225         verifier.setSystemProperty(REMOTE_URL_PROPERTY_NAME, url);
226         verifier.setSystemProperty(REMOTE_SERVER_ID_PROPERTY_NAME, REPO_ID);
227         verifier.setLogFileName("../log-4.txt");
228         verifier.executeGoals(Arrays.asList("clean", "install"));
229         verifier.verifyErrorFreeLog();
230 
231         assertTrue(hasBuildInfoXml(localCache), () -> error(localCache, "local", true));
232         assertTrue(hasBuildInfoXml(remoteCache), () -> error(remoteCache, "remote", true));
233     }
234 
235     private boolean hasBuildInfoXml(Path cache) throws IOException {
236         return Files.walk(cache).anyMatch(isBuildInfoXml());
237     }
238 
239     @NotNull
240     private Predicate<Path> isBuildInfoXml() {
241         return p -> p.getFileName().toString().equals("buildinfo.xml");
242     }
243 
244     private void cleanDirs(Path... paths) throws IOException {
245         for (Path path : paths) {
246             IntegrationTestExtension.deleteDir(path);
247             Files.createDirectories(path);
248             Runtime.getRuntime().exec("chmod go+rwx " + path);
249         }
250     }
251 
252     private static void substitute(Path path, String... strings) throws IOException {
253         String str = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
254         for (int i = 0; i < strings.length / 2; i++) {
255             str = str.replaceAll(Pattern.quote("${" + strings[i * 2] + "}"), strings[i * 2 + 1]);
256         }
257         Files.deleteIfExists(path);
258         Files.write(path, str.getBytes(StandardCharsets.UTF_8));
259     }
260 
261     private String error(Path directory, String cache, boolean shouldHave) {
262         StringBuilder sb =
263                 new StringBuilder("The " + cache + " cache should " + (shouldHave ? "" : "not ") + "contain a build\n");
264         try {
265             sb.append("Contents:\n");
266             Files.walk(directory).forEach(p -> sb.append("    ").append(p).append("\n"));
267 
268             for (Path log : Files.list(logDir)
269                     .filter(p -> p.getFileName().toString().matches("log.*\\.txt"))
270                     .collect(Collectors.toList())) {
271                 sb.append("Log file: ").append(log).append("\n");
272                 Files.lines(log).forEach(l -> sb.append("    ").append(l).append("\n"));
273             }
274 
275             sb.append("Container log:\n");
276             Stream.of(dav.getLogs().split("\n"))
277                     .forEach(l -> sb.append("    ").append(l).append("\n"));
278 
279             sb.append("Remote cache listing:\n");
280             ls(remoteCache, s -> sb.append("    ").append(s).append("\n"));
281         } catch (IOException e) {
282             sb.append("Error: ").append(e);
283         }
284         return sb.toString();
285     }
286 
287     private static void ls(Path currentDir, Consumer<String> out) throws IOException {
288         class PathEntry implements Comparable<PathEntry> {
289 
290             final Path abs;
291             final Path path;
292             final Map<String, Object> attributes;
293 
294             public PathEntry(Path abs, Path root) {
295                 this.abs = abs;
296                 this.path = abs.startsWith(root) ? root.relativize(abs) : abs;
297                 this.attributes = readAttributes(abs);
298             }
299 
300             @Override
301             public int compareTo(PathEntry o) {
302                 return path.toString().compareTo(o.path.toString());
303             }
304 
305             boolean isNotDirectory() {
306                 return is("isRegularFile") || is("isSymbolicLink") || is("isOther");
307             }
308 
309             boolean isDirectory() {
310                 return is("isDirectory");
311             }
312 
313             private boolean is(String attr) {
314                 Object d = attributes.get(attr);
315                 return d instanceof Boolean && (Boolean) d;
316             }
317 
318             String display() {
319                 String suffix;
320                 String link = "";
321                 if (is("isSymbolicLink")) {
322                     suffix = "@";
323                     try {
324                         Path l = Files.readSymbolicLink(abs);
325                         link = " -> " + l.toString();
326                     } catch (IOException e) {
327                         // ignore
328                     }
329                 } else if (is("isDirectory")) {
330                     suffix = "/";
331                 } else if (is("isExecutable")) {
332                     suffix = "*";
333                 } else if (is("isOther")) {
334                     suffix = "";
335                 } else {
336                     suffix = "";
337                 }
338                 return path.toString() + suffix + link;
339             }
340 
341             String longDisplay() {
342                 String username;
343                 if (attributes.containsKey("owner")) {
344                     username = Objects.toString(attributes.get("owner"), null);
345                 } else {
346                     username = "owner";
347                 }
348                 if (username.length() > 8) {
349                     username = username.substring(0, 8);
350                 } else {
351                     for (int i = username.length(); i < 8; i++) {
352                         username = username + " ";
353                     }
354                 }
355                 String group;
356                 if (attributes.containsKey("group")) {
357                     group = Objects.toString(attributes.get("group"), null);
358                 } else {
359                     group = "group";
360                 }
361                 if (group.length() > 8) {
362                     group = group.substring(0, 8);
363                 } else {
364                     for (int i = group.length(); i < 8; i++) {
365                         group = group + " ";
366                     }
367                 }
368                 Number length = (Number) attributes.get("size");
369                 if (length == null) {
370                     length = 0L;
371                 }
372                 String lengthString;
373                 if (true /*opt.isSet("h")*/) {
374                     double l = length.longValue();
375                     String unit = "B";
376                     if (l >= 1000) {
377                         l /= 1024;
378                         unit = "K";
379                         if (l >= 1000) {
380                             l /= 1024;
381                             unit = "M";
382                             if (l >= 1000) {
383                                 l /= 1024;
384                                 unit = "T";
385                             }
386                         }
387                     }
388                     if (l < 10 && length.longValue() > 1000) {
389                         lengthString = String.format("%.1f", l) + unit;
390                     } else {
391                         lengthString = String.format("%3.0f", l) + unit;
392                     }
393                 } else {
394                     lengthString = String.format("%1$8s", length);
395                 }
396                 @SuppressWarnings("unchecked")
397                 Set<PosixFilePermission> perms = (Set<PosixFilePermission>) attributes.get("permissions");
398                 if (perms == null) {
399                     perms = EnumSet.noneOf(PosixFilePermission.class);
400                 }
401                 // TODO: all fields should be padded to align
402                 return (is("isDirectory") ? "d" : (is("isSymbolicLink") ? "l" : (is("isOther") ? "o" : "-")))
403                         + PosixFilePermissions.toString(perms) + " "
404                         + String.format(
405                                 "%3s",
406                                 (attributes.containsKey("nlink")
407                                         ? attributes.get("nlink").toString()
408                                         : "1"))
409                         + " " + username + " " + group + " " + lengthString + " "
410                         + toString((FileTime) attributes.get("lastModifiedTime"))
411                         + " " + display();
412             }
413 
414             protected String toString(FileTime time) {
415                 long millis = (time != null) ? time.toMillis() : -1L;
416                 if (millis < 0L) {
417                     return "------------";
418                 }
419                 ZonedDateTime dt = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault());
420                 // Less than six months
421                 if (System.currentTimeMillis() - millis < 183L * 24L * 60L * 60L * 1000L) {
422                     return DateTimeFormatter.ofPattern("MMM ppd HH:mm").format(dt);
423                 }
424                 // Older than six months
425                 else {
426                     return DateTimeFormatter.ofPattern("MMM ppd  yyyy").format(dt);
427                 }
428             }
429 
430             protected Map<String, Object> readAttributes(Path path) {
431                 Map<String, Object> attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
432                 for (String view : path.getFileSystem().supportedFileAttributeViews()) {
433                     try {
434                         Map<String, Object> ta = Files.readAttributes(path, view + ":*", LinkOption.NOFOLLOW_LINKS);
435                         ta.forEach(attrs::putIfAbsent);
436                     } catch (IOException e) {
437                         // Ignore
438                     }
439                 }
440                 attrs.computeIfAbsent("isExecutable", s -> Files.isExecutable(path));
441                 attrs.computeIfAbsent("permissions", s -> getPermissionsFromFile(path.toFile()));
442                 return attrs;
443             }
444         }
445 
446         Files.walk(currentDir)
447                 .map(p -> new PathEntry(p, currentDir))
448                 .sorted()
449                 .map(PathEntry::longDisplay)
450                 .forEach(out);
451     }
452 
453     private static Set<PosixFilePermission> getPermissionsFromFile(File f) {
454         Set<PosixFilePermission> perms = EnumSet.noneOf(PosixFilePermission.class);
455         if (f.canRead()) {
456             perms.add(PosixFilePermission.OWNER_READ);
457             perms.add(PosixFilePermission.GROUP_READ);
458             perms.add(PosixFilePermission.OTHERS_READ);
459         }
460 
461         if (f.canWrite()) {
462             perms.add(PosixFilePermission.OWNER_WRITE);
463             perms.add(PosixFilePermission.GROUP_WRITE);
464             perms.add(PosixFilePermission.OTHERS_WRITE);
465         }
466 
467         if (f.canExecute() /*|| (OSUtils.IS_WINDOWS && isWindowsExecutable(f.getName()))*/) {
468             perms.add(PosixFilePermission.OWNER_EXECUTE);
469             perms.add(PosixFilePermission.GROUP_EXECUTE);
470             perms.add(PosixFilePermission.OTHERS_EXECUTE);
471         }
472 
473         return perms;
474     }
475 }