1 /*
  2  * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 package org.openjdk.skara.bot;
 24 
 25 import org.openjdk.skara.host.*;
 26 import org.openjdk.skara.host.network.URIBuilder;
 27 import org.openjdk.skara.json.JSONObject;
 28 import org.openjdk.skara.vcs.VCS;
 29 
 30 import java.io.*;
 31 import java.net.URI;
 32 import java.nio.file.*;
 33 import java.time.Duration;
 34 import java.util.*;
 35 import java.util.logging.Logger;
 36 import java.util.regex.Pattern;
 37 
 38 public class BotRunnerConfiguration {
 39     private final Logger log;
 40     private final JSONObject config;
 41     private final Map<String, Host> hosts;
 42     private final Map<String, HostedRepository> repositories;
 43 
 44     private BotRunnerConfiguration(JSONObject config, Path cwd) throws ConfigurationError {
 45         this.config = config;
 46         log = Logger.getLogger("org.openjdk.skara.bot");
 47 
 48         hosts = parseHosts(config, cwd);
 49         repositories = parseRepositories(config);
 50     }
 51 
 52     private Map<String, Host> parseHosts(JSONObject config, Path cwd) throws ConfigurationError {
 53         Map<String, Host> ret = new HashMap<>();
 54 
 55         if (!config.contains("hosts")) {
 56             return ret;
 57         }
 58 
 59         for (var entry : config.get("hosts").fields()) {
 60             if (entry.value().contains("gitlab")) {
 61                 var gitlab = entry.value().get("gitlab");
 62                 var uri = URIBuilder.base(gitlab.get("url").asString()).build();
 63                 var pat = new PersonalAccessToken(gitlab.get("username").asString(), gitlab.get("pat").asString());
 64                 ret.put(entry.name(), HostFactory.createGitLabHost(uri, pat));
 65             } else if (entry.value().contains("github")) {
 66                 var github = entry.value().get("github");
 67                 URI uri;
 68                 if (github.contains("url")) {
 69                     uri = URIBuilder.base(github.get("url").asString()).build();
 70                 } else {
 71                     uri = URIBuilder.base("https://github.com/").build();
 72                 }
 73                 Pattern webUriPattern = null;
 74                 String webUriReplacement = null;
 75                 if (github.contains("weburl")) {
 76                     webUriPattern = Pattern.compile(github.get("weburl").get("pattern").asString());
 77                     webUriReplacement = github.get("weburl").get("replacement").asString();
 78                 }
 79 
 80                 if (github.contains("app")) {
 81                     var keyFile = cwd.resolve(github.get("app").get("key").asString());
 82                     ret.put(entry.name(), HostFactory.createGitHubHost(uri, webUriPattern, webUriReplacement, keyFile.toString(),
 83                                                                        github.get("app").get("id").asString(),
 84                                                                        github.get("app").get("installation").asString()));
 85                 } else {
 86                     var pat = new PersonalAccessToken(github.get("username").asString(), github.get("pat").asString());
 87                     ret.put(entry.name(), HostFactory.createGitHubHost(uri, pat));
 88                 }
 89             } else {
 90                 throw new ConfigurationError("Host " + entry.name());
 91             }
 92         }
 93 
 94         return ret;
 95     }
 96 
 97     private Map<String, HostedRepository> parseRepositories(JSONObject config) throws ConfigurationError {
 98         Map<String, HostedRepository> ret = new HashMap<>();
 99 
100         if (!config.contains("repositories")) {
101             return ret;
102         }
103 
104         for (var entry : config.get("repositories").fields()) {
105             var hostName = entry.value().get("host").asString();
106             if (!hosts.containsKey(hostName)) {
107                 throw new ConfigurationError("Repository " + entry.name() + " uses undefined host '" + hostName + "'");
108             }
109             var host = hosts.get(hostName);
110             var repo = host.getRepository(entry.value().get("repository").asString());
111             ret.put(entry.name(), repo);
112         }
113 
114         return ret;
115     }
116 
117     private static class RepositoryEntry {
118         HostedRepository repository;
119         String ref;
120     }
121 
122     private RepositoryEntry parseRepositoryEntry(String entry) throws ConfigurationError {
123         var ret = new RepositoryEntry();
124         var refSeparatorIndex = entry.indexOf(':');
125         if (refSeparatorIndex >= 0) {
126             ret.ref = entry.substring(refSeparatorIndex + 1);
127             entry = entry.substring(0, refSeparatorIndex);
128         }
129         var hostSeparatorIndex = entry.indexOf('/');
130         if (hostSeparatorIndex >= 0) {
131             var hostName = entry.substring(0, hostSeparatorIndex);
132             var host = hosts.get(hostName);
133             if (!hosts.containsKey(hostName)) {
134                 throw new ConfigurationError("Repository entry " + entry + " uses undefined host '" + hostName + "'");
135             }
136             var repositoryName = entry.substring(hostSeparatorIndex + 1);
137             ret.repository = host.getRepository(repositoryName);
138         } else {
139             if (!repositories.containsKey(entry)) {
140                 throw new ConfigurationError("Repository " + entry + " is not defined!");
141             }
142             ret.repository = repositories.get(entry);
143         }
144 
145         if (ret.ref == null) {
146             ret.ref = ret.repository.getRepositoryType() == VCS.GIT ? "master" : "default";
147         }
148 
149         return ret;
150     }
151 
152     public static BotRunnerConfiguration parse(JSONObject config, Path cwd) throws ConfigurationError {
153         return new BotRunnerConfiguration(config, cwd);
154     }
155 
156     public static BotRunnerConfiguration parse(JSONObject config) throws ConfigurationError {
157         return parse(config, Paths.get("."));
158     }
159 
160     public BotConfiguration perBotConfiguration(String botName) throws ConfigurationError {
161         if (!config.contains(botName)) {
162             throw new ConfigurationError("No configuration for bot name: " + botName);
163         }
164 
165         return new BotConfiguration() {
166             @Override
167             public Path storageFolder() {
168                 if (!config.contains("storage") || !config.get("storage").contains("path")) {
169                     try {
170                         return Files.createTempDirectory("storage-" + botName);
171                     } catch (IOException e) {
172                         throw new UncheckedIOException(e);
173                     }
174                 }
175                 return Paths.get(config.get("storage").get("path").asString()).resolve(botName);
176             }
177 
178             @Override
179             public HostedRepository repository(String name) {
180                 try {
181                     var entry = parseRepositoryEntry(name);
182                     return entry.repository;
183                 } catch (ConfigurationError configurationError) {
184                     throw new RuntimeException("Couldn't find repository with name: " + name, configurationError);
185                 }
186             }
187 
188             @Override
189             public String repositoryRef(String name) {
190                 try {
191                     var entry = parseRepositoryEntry(name);
192                     return entry.ref;
193                 } catch (ConfigurationError configurationError) {
194                     throw new RuntimeException("Couldn't find repository with name: " + name, configurationError);
195                 }
196             }
197 
198             @Override
199             public String repositoryName(String name) {
200                 var refIndex = name.indexOf(':');
201                 if (refIndex >= 0) {
202                     name = name.substring(0, refIndex);
203                 }
204                 var orgIndex = name.lastIndexOf('/');
205                 if (orgIndex >= 0) {
206                     name = name.substring(orgIndex + 1);
207                 }
208                 return name;
209             }
210 
211             @Override
212             public JSONObject specific() {
213                 return config.get(botName).asObject();
214             }
215         };
216     }
217 
218     /**
219      * The amount of time to wait between each invocation of Bot.getPeriodicItems.
220      * @return
221      */
222     Duration scheduledExecutionPeriod() {
223         if (!config.contains("runner") || !config.get("runner").contains("interval")) {
224             log.info("No WorkItem invocation period defined, using default value");
225             return Duration.ofSeconds(10);
226         } else {
227             return Duration.parse(config.get("runner").get("interval").asString());
228         }
229     }
230 
231     /**
232      * Number of WorkItems to execute in parallel.
233      * @return
234      */
235     Integer concurrency() {
236         if (!config.contains("runner") || !config.get("runner").contains("concurrency")) {
237             log.info("WorkItem concurrency not defined, using default value");
238             return 2;
239         } else {
240             return config.get("runner").get("concurrency").asInt();
241         }
242     }
243 
244     /**
245      * Folder that WorkItems may use to store temporary data.
246      * @return
247      */
248     Path scratchFolder() {
249         if (!config.contains("scratch") || !config.get("scratch").contains("path")) {
250             try {
251                 log.warning("No scratch folder defined, creating a temporary folder");
252                 return Files.createTempDirectory("botrunner");
253             } catch (IOException e) {
254                 throw new UncheckedIOException(e);
255             }
256         }
257         return Paths.get(config.get("scratch").get("path").asString());
258     }
259 
260     Optional<Integer> restReceiverPort() {
261         if (!config.contains("webhooks")) {
262             return Optional.empty();
263         }
264         return Optional.of(config.get("webhooks").get("port").asInt());
265     }
266 
267     Duration watchdogTimeout() {
268         if (!config.contains("runner") || !config.get("runner").contains("watchdog")) {
269             log.info("No WorkItem watchdog timeout defined, using default value");
270             return Duration.ofHours(1);
271         } else {
272             return Duration.parse(config.get("runner").get("watchdog").asString());
273         }
274     }
275 }