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.census.*; 26 import org.openjdk.skara.host.*; 27 import org.openjdk.skara.host.network.URIBuilder; 28 import org.openjdk.skara.json.*; 29 import org.openjdk.skara.vcs.*; 30 31 import java.io.*; 32 import java.net.URI; 33 import java.nio.file.*; 34 import java.time.Duration; 35 import java.util.*; 36 import java.util.logging.*; 37 import java.util.regex.Pattern; 38 39 public class BotRunnerConfiguration { 40 private final Logger log; 41 private final JSONObject config; 42 private final Map<String, Host> hosts; 43 private final Map<String, HostedRepository> repositories; 44 45 private BotRunnerConfiguration(JSONObject config, Path cwd) throws ConfigurationError { 46 this.config = config; 47 log = Logger.getLogger("org.openjdk.skara.bot"); 48 49 hosts = parseHosts(config, cwd); 50 repositories = parseRepositories(config); 51 } 52 53 private Map<String, Host> parseHosts(JSONObject config, Path cwd) throws ConfigurationError { 54 Map<String, Host> ret = new HashMap<>(); 55 56 if (!config.contains("hosts")) { 57 return ret; 58 } 59 60 for (var entry : config.get("hosts").fields()) { 61 if (entry.value().contains("gitlab")) { 62 var gitlab = entry.value().get("gitlab"); 63 var uri = URIBuilder.base(gitlab.get("url").asString()).build(); 64 var pat = new PersonalAccessToken(gitlab.get("username").asString(), gitlab.get("pat").asString()); 65 ret.put(entry.name(), HostFactory.createGitLabHost(uri, pat)); 66 } else if (entry.value().contains("github")) { 67 var github = entry.value().get("github"); 68 URI uri; 69 if (github.contains("url")) { 70 uri = URIBuilder.base(github.get("url").asString()).build(); 71 } else { 72 uri = URIBuilder.base("https://github.com/").build(); 73 } 74 Pattern webUriPattern = null; 75 String webUriReplacement = null; 76 if (github.contains("weburl")) { 77 webUriPattern = Pattern.compile(github.get("weburl").get("pattern").asString()); 78 webUriReplacement = github.get("weburl").get("replacement").asString(); 79 } 80 81 if (github.contains("app")) { 82 var keyFile = cwd.resolve(github.get("app").get("key").asString()); 83 ret.put(entry.name(), HostFactory.createGitHubHost(uri, webUriPattern, webUriReplacement, keyFile.toString(), 84 github.get("app").get("id").asString(), 85 github.get("app").get("installation").asString())); 86 } else { 87 var pat = new PersonalAccessToken(github.get("username").asString(), github.get("pat").asString()); 88 ret.put(entry.name(), HostFactory.createGitHubHost(uri, pat)); 89 } 90 } else { 91 throw new ConfigurationError("Host " + entry.name()); 92 } 93 } 94 95 return ret; 96 } 97 98 private Map<String, HostedRepository> parseRepositories(JSONObject config) throws ConfigurationError { 99 Map<String, HostedRepository> ret = new HashMap<>(); 100 101 if (!config.contains("repositories")) { 102 return ret; 103 } 104 105 for (var entry : config.get("repositories").fields()) { 106 var hostName = entry.value().get("host").asString(); 107 if (!hosts.containsKey(hostName)) { 108 throw new ConfigurationError("Repository " + entry.name() + " uses undefined host '" + hostName + "'"); 109 } 110 var host = hosts.get(hostName); 111 var repo = host.getRepository(entry.value().get("repository").asString()); 112 ret.put(entry.name(), repo); 113 } 114 115 return ret; 116 } 117 118 private static class RepositoryEntry { 119 HostedRepository repository; 120 String ref; 121 } 122 123 private RepositoryEntry parseRepositoryEntry(String entry) throws ConfigurationError { 124 var ret = new RepositoryEntry(); 125 var refSeparatorIndex = entry.indexOf(':'); 126 if (refSeparatorIndex >= 0) { 127 ret.ref = entry.substring(refSeparatorIndex + 1); 128 entry = entry.substring(0, refSeparatorIndex); 129 } 130 var hostSeparatorIndex = entry.indexOf('/'); 131 if (hostSeparatorIndex >= 0) { 132 var hostName = entry.substring(0, hostSeparatorIndex); 133 var host = hosts.get(hostName); 134 if (!hosts.containsKey(hostName)) { 135 throw new ConfigurationError("Repository entry " + entry + " uses undefined host '" + hostName + "'"); 136 } 137 var repositoryName = entry.substring(hostSeparatorIndex + 1); 138 ret.repository = host.getRepository(repositoryName); 139 } else { 140 if (!repositories.containsKey(entry)) { 141 throw new ConfigurationError("Repository " + entry + " is not defined!"); 142 } 143 ret.repository = repositories.get(entry); 144 } 145 146 if (ret.ref == null) { 147 ret.ref = ret.repository.getRepositoryType() == VCS.GIT ? "master" : "default"; 148 } 149 150 return ret; 151 } 152 153 public static BotRunnerConfiguration parse(JSONObject config, Path cwd) throws ConfigurationError { 154 return new BotRunnerConfiguration(config, cwd); 155 } 156 157 public static BotRunnerConfiguration parse(JSONObject config) throws ConfigurationError { 158 return parse(config, Paths.get(".")); 159 } 160 161 public BotConfiguration perBotConfiguration(String botName) throws ConfigurationError { 162 if (!config.contains(botName)) { 163 throw new ConfigurationError("No configuration for bot name: " + botName); 164 } 165 166 return new BotConfiguration() { 167 @Override 168 public Path storageFolder() { 169 if (!config.contains("storage") || !config.get("storage").contains("path")) { 170 try { 171 return Files.createTempDirectory("storage-" + botName); 172 } catch (IOException e) { 173 throw new UncheckedIOException(e); 174 } 175 } 176 return Paths.get(config.get("storage").get("path").asString()).resolve(botName); 177 } 178 179 @Override 180 public HostedRepository repository(String name) { 181 try { 182 var entry = parseRepositoryEntry(name); 183 return entry.repository; 184 } catch (ConfigurationError configurationError) { 185 throw new RuntimeException("Couldn't find repository with name: " + name, configurationError); 186 } 187 } 188 189 @Override 190 public String repositoryRef(String name) { 191 try { 192 var entry = parseRepositoryEntry(name); 193 return entry.ref; 194 } catch (ConfigurationError configurationError) { 195 throw new RuntimeException("Couldn't find repository with name: " + name, configurationError); 196 } 197 } 198 199 @Override 200 public JSONObject specific() { 201 return config.get(botName).asObject(); 202 } 203 }; 204 } 205 206 /** 207 * The amount of time to wait between each invocation of Bot.getPeriodicItems. 208 * @return 209 */ 210 Duration scheduledExecutionPeriod() { 211 if (!config.contains("runner") || !config.get("runner").contains("interval")) { 212 log.info("No WorkItem invocation period defined, using default value"); 213 return Duration.ofSeconds(10); 214 } else { 215 return Duration.parse(config.get("runner").get("interval").asString()); 216 } 217 } 218 219 /** 220 * Number of WorkItems to execute in parallel. 221 * @return 222 */ 223 Integer concurrency() { 224 if (!config.contains("runner") || !config.get("runner").contains("concurrency")) { 225 log.info("WorkItem concurrency not defined, using default value"); 226 return 2; 227 } else { 228 return config.get("runner").get("concurrency").asInt(); 229 } 230 } 231 232 /** 233 * Folder that WorkItems may use to store temporary data. 234 * @return 235 */ 236 Path scratchFolder() { 237 if (!config.contains("scratch") || !config.get("scratch").contains("path")) { 238 try { 239 log.warning("No scratch folder defined, creating a temporary folder"); 240 return Files.createTempDirectory("botrunner"); 241 } catch (IOException e) { 242 throw new UncheckedIOException(e); 243 } 244 } 245 return Paths.get(config.get("scratch").get("path").asString()); 246 } 247 248 Optional<Integer> restReceiverPort() { 249 if (!config.contains("webhooks")) { 250 return Optional.empty(); 251 } 252 return Optional.of(config.get("webhooks").get("port").asInt()); 253 } 254 }