Detemine number of nexus repos to support
[policy/drools-pdp.git] / feature-state-management / src / main / java / org / onap / policy / drools / statemanagement / RepositoryAudit.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * feature-state-management
4  * ================================================================================
5  * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.policy.drools.statemanagement;
22
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.nio.file.FileVisitResult;
28 import java.nio.file.Files;
29 import java.nio.file.Path;
30 import java.nio.file.SimpleFileVisitor;
31 import java.nio.file.attribute.BasicFileAttributes;
32 import java.util.LinkedList;
33 import java.util.List;
34 import java.util.Properties;
35 import java.util.Set;
36 import java.util.TreeSet;
37 import java.util.concurrent.TimeUnit;
38 import java.util.regex.Matcher;
39 import java.util.regex.Pattern;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 /**
44  * This class audits the Maven repository.
45  */
46 public class RepositoryAudit extends DroolsPdpIntegrityMonitor.AuditBase {
47     // timeout in 60 seconds
48     private static final long DEFAULT_TIMEOUT = 60;
49
50     // get an instance of logger
51     private static final Logger logger = LoggerFactory.getLogger(RepositoryAudit.class);
52
53     // single global instance of this audit object
54     private static RepositoryAudit instance = new RepositoryAudit();
55
56     // Regex pattern used to find additional repos in the form "repository(number).id.url"
57     private static final Pattern repoPattern = Pattern.compile("(repository([1-9][0-9]*))[.]audit[.]id");
58
59     /**
60      * Constructor - set the name to 'Repository'.
61      */
62     private RepositoryAudit() {
63         super("Repository");
64     }
65
66     /**
67      * Get the integrity monitor instance.
68      *
69      * @return the single 'RepositoryAudit' instance
70      */
71     public static DroolsPdpIntegrityMonitor.AuditBase getInstance() {
72         return instance;
73     }
74
75     /**
76      * First, get the names of each property from StateManagementProperties. For each property name, check if it is of
77      * the form "repository(number).audit.id" If so, we extract the number and determine if there exists another
78      * property in the form "repository(number).audit.url" with the same "number". Only the
79      * 'repository(number).audit.id' and 'repository(number).audit.url" properties need to be specified. If both 'id'
80      * and 'url' properties are found, we add it to our set. InvokeData.getProperty(String, boolean) will determine the
81      * other 4 properties: '*.username', '*.password', '*.is.active', and '*.ignore.errors', or use default values.
82      *
83      * @return set of Integers representing a repository to support
84      */
85     private static TreeSet<Integer> countAdditionalNexusRepos() {
86         TreeSet<Integer> returnIndices = new TreeSet<>();
87         Properties properties = StateManagementProperties.getProperties();
88         Set<String> propertyNames = properties.stringPropertyNames();
89
90         for (String currName : propertyNames) {
91             Matcher matcher = repoPattern.matcher(currName);
92
93             if (matcher.matches()) {
94                 int currRepoNum = Integer.parseInt(matcher.group(2));
95                 if (propertyNames.contains(matcher.group(1) + ".audit.url")) {
96                     returnIndices.add(currRepoNum);
97                 }
98             }
99         }
100         return returnIndices;
101     }
102
103     /**
104      * Invoke the audit.
105      *
106      * @param properties properties to be passed to the audit
107      */
108     @Override
109     public void invoke(Properties properties) throws IOException, InterruptedException {
110         logger.debug("Running 'RepositoryAudit.invoke'");
111
112         InvokeData data = new InvokeData();
113
114         logger.debug("RepositoryAudit.invoke: repoAuditIsActive = {}" + ", repoAuditIgnoreErrors = {}",
115                 data.repoAuditIsActive, data.repoAuditIgnoreErrors);
116
117         data.initIsActive();
118
119         if (!data.isActive) {
120             logger.info("RepositoryAudit.invoke: exiting because isActive = {}", data.isActive);
121             return;
122         }
123
124         // Run audit for first nexus repository
125         logger.debug("Running read-only audit on first nexus repository: repository");
126         runAudit(data);
127
128         // set of indices for supported nexus repos (ex: repository2 -> 2)
129         // TreeSet is used to maintain order so repos can be audited in numerical order
130         TreeSet<Integer> repoIndices = countAdditionalNexusRepos();
131         logger.debug("Additional nexus repositories: {}", repoIndices);
132
133         // Run audit for remaining 'numNexusRepos' repositories
134         for (int index : repoIndices) {
135             logger.debug("Running read-only audit on nexus repository = repository{}", index);
136
137             data = new InvokeData(index);
138             data.initIsActive();
139
140             if (data.isActive) {
141                 runAudit(data);
142             }
143         }
144     }
145
146     private void runAudit(InvokeData data) throws IOException, InterruptedException {
147         data.initIgnoreErrors();
148         data.initTimeout();
149
150         /*
151          * 1) create temporary directory
152          */
153         data.dir = Files.createTempDirectory("auditRepo");
154         logger.info("RepositoryAudit: temporary directory = {}", data.dir);
155
156         // nested 'pom.xml' file and 'repo' directory
157         final Path pom = data.dir.resolve("pom.xml");
158         final Path repo = data.dir.resolve("repo");
159
160         /*
161          * 2) Create test file, and upload to repository (only if repository information is specified)
162          */
163         if (data.upload) {
164             data.uploadTestFile();
165         }
166
167         /*
168          * 3) create 'pom.xml' file in temporary directory
169          */
170         data.createPomFile(repo, pom);
171
172         /*
173          * 4) Invoke external 'mvn' process to do the downloads
174          */
175
176         // output file = ${dir}/out (this supports step '4a')
177         File output = data.dir.resolve("out").toFile();
178
179         // invoke process, and wait for response
180         int rval = data.runMaven(output);
181
182         /*
183          * 4a) Check attempted and successful downloads from output file Note: at present, this step just generates log
184          * messages, but doesn't do any verification.
185          */
186         if (rval == 0 && output != null) {
187             generateDownloadLogs(output);
188         }
189
190         /*
191          * 5) Check the contents of the directory to make sure the downloads were successful
192          */
193         data.verifyDownloads(repo);
194
195         /*
196          * 6) Use 'curl' to delete the uploaded test file (only if repository information is specified)
197          */
198         if (data.upload) {
199             data.deleteUploadedTestFile();
200         }
201
202         /*
203          * 7) Remove the temporary directory
204          */
205         Files.walkFileTree(data.dir, new RecursivelyDeleteDirectory());
206     }
207
208
209     /**
210      * Set the response string to the specified value. Overrides 'setResponse(String value)' from
211      * DroolsPdpIntegrityMonitor This method prevents setting a response string that indicates whether the caller should
212      * receive an error list from the audit. By NOT setting the response string to a value, this indicates that there
213      * are no errors.
214      *
215      * @param value the new value of the response string (null = no errors)
216      */
217     @Override
218     public void setResponse(String value) {
219         // Do nothing, prevent the caller from receiving a list of errors.
220     }
221
222     private class InvokeData {
223         private boolean isActive = true;
224
225         // ignore errors by default
226         private boolean ignoreErrors = true;
227
228         private final String repoAuditIsActive;
229         private final String repoAuditIgnoreErrors;
230
231         private final String repositoryId;
232         private final String repositoryUrl;
233         private final String repositoryUsername;
234         private final String repositoryPassword;
235         private final boolean upload;
236
237         // used to incrementally construct response as problems occur
238         // (empty = no problems)
239         private final StringBuilder response = new StringBuilder();
240
241         private long timeoutInSeconds = DEFAULT_TIMEOUT;
242
243         private Path dir;
244
245         private String groupId = null;
246         private String artifactId = null;
247         private String version = null;
248
249         // artifacts to be downloaded
250         private final List<Artifact> artifacts = new LinkedList<>();
251
252         // 0 = base repository, 2-n = additional repositories
253         private final int index;
254
255         public InvokeData() {
256             this(0);
257         }
258
259         public InvokeData(int index) {
260             this.index = index;
261             repoAuditIsActive = getProperty("audit.is.active", true);
262             repoAuditIgnoreErrors = getProperty("audit.ignore.errors", true);
263
264             // Fetch repository information from 'IntegrityMonitorProperties'
265             repositoryId = getProperty("audit.id", false);
266             repositoryUrl = getProperty("audit.url", false);
267             repositoryUsername = getProperty("audit.username", true);
268             repositoryPassword = getProperty("audit.password", true);
269
270             logger.debug("Nexus Repository Information retrieved from 'IntegrityMonitorProperties':");
271             logger.debug("repositoryId: " + repositoryId);
272             logger.debug("repositoryUrl: " + repositoryUrl);
273
274             // Setting upload to be false so that files can no longer be created/deleted
275             upload = false;
276         }
277
278         private String getProperty(String property, boolean useDefault) {
279             String fullProperty = (index == 0 ? "repository." + property : "repository" + index + "." + property);
280             String rval = StateManagementProperties.getProperty(fullProperty);
281             if (rval == null && index != 0 && useDefault) {
282                 rval = StateManagementProperties.getProperty("repository." + property);
283             }
284             return rval;
285         }
286
287         public void initIsActive() {
288             if (repoAuditIsActive != null) {
289                 try {
290                     isActive = Boolean.parseBoolean(repoAuditIsActive.trim());
291                 } catch (NumberFormatException e) {
292                     logger.warn("RepositoryAudit.invoke: Ignoring invalid property: repository.audit.is.active = {}",
293                             repoAuditIsActive);
294                 }
295             }
296             if (repositoryId == null || repositoryUrl == null) {
297                 isActive = false;
298             }
299         }
300
301         public void initIgnoreErrors() {
302             if (repoAuditIgnoreErrors != null) {
303                 try {
304                     ignoreErrors = Boolean.parseBoolean(repoAuditIgnoreErrors.trim());
305                 } catch (NumberFormatException e) {
306                     ignoreErrors = true;
307                     logger.warn(
308                             "RepositoryAudit.invoke: Ignoring invalid property: repository.audit.ignore.errors = {}",
309                             repoAuditIgnoreErrors);
310                 }
311             } else {
312                 ignoreErrors = true;
313             }
314         }
315
316         public void initTimeout() {
317             String timeoutString = getProperty("audit.timeout", true);
318             if (timeoutString != null && !timeoutString.isEmpty()) {
319                 try {
320                     timeoutInSeconds = Long.valueOf(timeoutString);
321                 } catch (NumberFormatException e) {
322                     logger.error("RepositoryAudit: Invalid 'repository.audit.timeout' value: '{}'", timeoutString, e);
323                     if (!ignoreErrors) {
324                         response.append("Invalid 'repository.audit.timeout' value: '").append(timeoutString)
325                                 .append("'\n");
326                         setResponse(response.toString());
327                     }
328                 }
329             }
330         }
331
332         private void uploadTestFile() throws IOException, InterruptedException {
333             groupId = "org.onap.policy.audit";
334             artifactId = "repository-audit";
335             version = "0." + System.currentTimeMillis();
336
337             if (repositoryUrl.toLowerCase().contains("snapshot")) {
338                 // use SNAPSHOT version
339                 version += "-SNAPSHOT";
340             }
341
342             // create text file to write
343             try (FileOutputStream fos = new FileOutputStream(dir.resolve("repository-audit.txt").toFile())) {
344                 fos.write(version.getBytes());
345             }
346
347             // try to install file in repository
348             if (runProcess(timeoutInSeconds, dir.toFile(), null, "mvn", "deploy:deploy-file",
349                     "-DrepositoryId=" + repositoryId, "-Durl=" + repositoryUrl, "-Dfile=repository-audit.txt",
350                     "-DgroupId=" + groupId, "-DartifactId=" + artifactId, "-Dversion=" + version, "-Dpackaging=txt",
351                     "-DgeneratePom=false") != 0) {
352                 logger.error("RepositoryAudit: 'mvn deploy:deploy-file' failed");
353                 if (!ignoreErrors) {
354                     response.append("'mvn deploy:deploy-file' failed\n");
355                     setResponse(response.toString());
356                 }
357             } else {
358                 logger.info("RepositoryAudit: 'mvn deploy:deploy-file succeeded");
359
360                 // we also want to include this new artifact in the download
361                 // test (steps 3 and 4)
362                 artifacts.add(new Artifact(groupId, artifactId, version, "txt"));
363             }
364         }
365
366         private void createPomFile(final Path repo, final Path pom) throws IOException {
367
368             artifacts.add(new Artifact("org.apache.maven/maven-embedder/3.2.2"));
369
370             StringBuilder sb = new StringBuilder();
371             sb.append(
372                     "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
373                             + "         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n"
374                             + "\n" + "  <modelVersion>4.0.0</modelVersion>\n" + "  <groupId>empty</groupId>\n"
375                             + "  <artifactId>empty</artifactId>\n" + "  <version>1.0-SNAPSHOT</version>\n"
376                             + "  <packaging>pom</packaging>\n" + "\n" + "  <build>\n" + "    <plugins>\n"
377                             + "      <plugin>\n" + "         <groupId>org.apache.maven.plugins</groupId>\n"
378                             + "         <artifactId>maven-dependency-plugin</artifactId>\n"
379                             + "         <version>2.10</version>\n" + "         <executions>\n"
380                             + "           <execution>\n" + "             <id>copy</id>\n" + "             <goals>\n"
381                             + "               <goal>copy</goal>\n" + "             </goals>\n"
382                             + "             <configuration>\n" + "               <localRepositoryDirectory>")
383                     .append(repo).append("</localRepositoryDirectory>\n").append("               <artifactItems>\n");
384
385             for (Artifact artifact : artifacts) {
386                 // each artifact results in an 'artifactItem' element
387                 sb.append("                 <artifactItem>\n" + "                   <groupId>").append(artifact.groupId)
388                         .append("</groupId>\n" + "                   <artifactId>").append(artifact.artifactId)
389                         .append("</artifactId>\n" + "                   <version>").append(artifact.version)
390                         .append("</version>\n" + "                   <type>").append(artifact.type)
391                         .append("</type>\n" + "                 </artifactItem>\n");
392             }
393             sb.append("               </artifactItems>\n" + "             </configuration>\n"
394                     + "           </execution>\n" + "         </executions>\n" + "      </plugin>\n"
395                     + "    </plugins>\n" + "  </build>\n" + "</project>\n");
396
397             try (FileOutputStream fos = new FileOutputStream(pom.toFile())) {
398                 fos.write(sb.toString().getBytes());
399             }
400         }
401
402         private int runMaven(File output) throws IOException, InterruptedException {
403             int rval = runProcess(timeoutInSeconds, dir.toFile(), output, "mvn", "compile");
404             logger.info("RepositoryAudit: 'mvn' return value = {}", rval);
405             if (rval != 0) {
406                 logger.error("RepositoryAudit: 'mvn compile' invocation failed");
407                 if (!ignoreErrors) {
408                     response.append("'mvn compile' invocation failed\n");
409                     setResponse(response.toString());
410                 }
411             }
412             return rval;
413         }
414
415         private void verifyDownloads(final Path repo) {
416             for (Artifact artifact : artifacts) {
417                 if (repo.resolve(artifact.groupId.replace('.', '/')).resolve(artifact.artifactId)
418                         .resolve(artifact.version)
419                         .resolve(artifact.artifactId + "-" + artifact.version + "." + artifact.type).toFile()
420                         .exists()) {
421                     // artifact exists, as expected
422                     logger.info("RepositoryAudit: {} : exists", artifact.toString());
423                 } else {
424                     // Audit ERROR: artifact download failed for some reason
425                     logger.error("RepositoryAudit: {}: does not exist", artifact.toString());
426                     if (!ignoreErrors) {
427                         response.append("Failed to download artifact: ").append(artifact).append('\n');
428                         setResponse(response.toString());
429                     }
430                 }
431             }
432         }
433
434         private void deleteUploadedTestFile() throws IOException, InterruptedException {
435             if (runProcess(timeoutInSeconds, dir.toFile(), null, "curl", "--request", "DELETE", "--user",
436                     repositoryUsername + ":" + repositoryPassword,
437                     repositoryUrl + "/" + groupId.replace('.', '/') + "/" + artifactId + "/" + version) != 0) {
438                 logger.error("RepositoryAudit: delete of uploaded artifact failed");
439                 if (!ignoreErrors) {
440                     response.append("delete of uploaded artifact failed\n");
441                     setResponse(response.toString());
442                 }
443             } else {
444                 logger.info("RepositoryAudit: delete of uploaded artifact succeeded");
445                 artifacts.add(new Artifact(groupId, artifactId, version, "txt"));
446             }
447         }
448     }
449
450     private void generateDownloadLogs(File output) throws IOException {
451         // place output in 'fileContents' (replacing the Return characters
452         // with Newline)
453         byte[] outputData = new byte[(int) output.length()];
454         String fileContents;
455         try (FileInputStream fis = new FileInputStream(output)) {
456             //
457             // Ideally this should be in a loop or even better use
458             // Java 8 nio functionality.
459             //
460             int bytesRead = fis.read(outputData);
461             logger.info("fileContents read {} bytes", bytesRead);
462             fileContents = new String(outputData).replace('\r', '\n');
463         }
464
465         // generate log messages from 'Downloading' and 'Downloaded'
466         // messages within the 'mvn' output
467         int index = 0;
468         while ((index = fileContents.indexOf("\nDown", index)) > 0) {
469             index += 5;
470             if (fileContents.regionMatches(index, "loading: ", 0, 9)) {
471                 index += 9;
472                 int endIndex = fileContents.indexOf('\n', index);
473                 logger.info("RepositoryAudit: Attempted download: '{}'", fileContents.substring(index, endIndex));
474                 index = endIndex;
475             } else if (fileContents.regionMatches(index, "loaded: ", 0, 8)) {
476                 index += 8;
477                 int endIndex = fileContents.indexOf(' ', index);
478                 logger.info("RepositoryAudit: Successful download: '{}'", fileContents.substring(index, endIndex));
479                 index = endIndex;
480             }
481         }
482     }
483
484     /**
485      * Run a process, and wait for the response.
486      *
487      * @param timeoutInSeconds the number of seconds to wait for the process to terminate
488      * @param directory the execution directory of the process (null = current directory)
489      * @param stdout the file to contain the standard output (null = discard standard output)
490      * @param command command and arguments
491      * @return the return value of the process
492      * @throws IOException InterruptedException
493      */
494     static int runProcess(long timeoutInSeconds, File directory, File stdout, String... command)
495             throws IOException, InterruptedException {
496         ProcessBuilder pb = new ProcessBuilder(command);
497         if (directory != null) {
498             pb.directory(directory);
499         }
500         if (stdout != null) {
501             pb.redirectOutput(stdout);
502         }
503
504         Process process = pb.start();
505         if (process.waitFor(timeoutInSeconds, TimeUnit.SECONDS)) {
506             // process terminated before the timeout
507             return process.exitValue();
508         }
509
510         // process timed out -- kill it, and return -1
511         process.destroyForcibly();
512         return -1;
513     }
514
515     /**
516      * This class is used to recursively delete a directory and all of its contents.
517      */
518     private final class RecursivelyDeleteDirectory extends SimpleFileVisitor<Path> {
519         @Override
520         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
521             file.toFile().delete();
522             return FileVisitResult.CONTINUE;
523         }
524
525         @Override
526         public FileVisitResult postVisitDirectory(Path file, IOException ex) throws IOException {
527             if (ex == null) {
528                 file.toFile().delete();
529                 return FileVisitResult.CONTINUE;
530             } else {
531                 throw ex;
532             }
533         }
534     }
535
536     /* ============================================================ */
537
538     /**
539      * An instance of this class exists for each artifact that we are trying to download.
540      */
541     static class Artifact {
542         String groupId;
543         String artifactId;
544         String version;
545         String type;
546
547         /**
548          * Constructor - populate the 'Artifact' instance.
549          *
550          * @param groupId groupId of artifact
551          * @param artifactId artifactId of artifact
552          * @param version version of artifact
553          * @param type type of the artifact (e.g. "jar")
554          */
555         Artifact(String groupId, String artifactId, String version, String type) {
556             this.groupId = groupId;
557             this.artifactId = artifactId;
558             this.version = version;
559             this.type = type;
560         }
561
562         /**
563          * Constructor - populate an 'Artifact' instance.
564          *
565          * @param artifact a string of the form: {@code"<groupId>/<artifactId>/<version>[/<type>]"}
566          * @throws IllegalArgumentException if 'artifact' has the incorrect format
567          */
568         Artifact(String artifact) {
569             String[] segments = artifact.split("/");
570             if (segments.length != 4 && segments.length != 3) {
571                 throw new IllegalArgumentException("groupId/artifactId/version/type");
572             }
573             groupId = segments[0];
574             artifactId = segments[1];
575             version = segments[2];
576             type = segments.length == 4 ? segments[3] : "jar";
577         }
578
579         /**
580          * Returns string representation.
581          *
582          * @return the artifact id in the form: {@code"<groupId>/<artifactId>/<version>/<type>"}
583          */
584         @Override
585         public String toString() {
586             return groupId + "/" + artifactId + "/" + version + "/" + type;
587         }
588     }
589 }