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 }