|
11 | 11 | * You may obtain a copy of the License at |
12 | 12 | * |
13 | 13 | * http://www.apache.org/licenses/LICENSE-2.0 |
14 | | - * |
| 14 | + * |
15 | 15 | * Unless required by applicable law or agreed to in writing, software |
16 | 16 | * distributed under the License is distributed on an "AS IS" BASIS, |
17 | 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
20 | 20 | * ===== |
21 | 21 | */ |
22 | 22 |
|
| 23 | +import com.walmartlabs.concord.db.MainDB; |
| 24 | +import com.walmartlabs.concord.sdk.Constants.Trigger; |
| 25 | +import com.walmartlabs.concord.sdk.MapUtils; |
| 26 | +import com.walmartlabs.concord.server.cfg.GithubConfiguration; |
| 27 | +import com.walmartlabs.concord.server.events.DefaultEventFilter; |
| 28 | +import com.walmartlabs.concord.server.org.project.RepositoryDao; |
| 29 | +import com.walmartlabs.concord.server.org.project.RepositoryEntry; |
23 | 30 | import com.walmartlabs.concord.server.org.triggers.TriggerEntry; |
| 31 | +import com.walmartlabs.concord.server.org.triggers.TriggerUtils; |
| 32 | +import com.walmartlabs.concord.server.org.triggers.TriggersDao; |
| 33 | +import com.walmartlabs.concord.server.sdk.metrics.WithTimer; |
| 34 | +import com.walmartlabs.concord.server.security.github.GithubKey; |
| 35 | +import org.jooq.Configuration; |
| 36 | +import org.jooq.DSLContext; |
| 37 | +import org.jooq.impl.DSL; |
| 38 | +import org.slf4j.Logger; |
| 39 | +import org.slf4j.LoggerFactory; |
24 | 40 |
|
| 41 | +import javax.inject.Inject; |
| 42 | +import javax.inject.Named; |
| 43 | +import javax.inject.Singleton; |
25 | 44 | import javax.ws.rs.core.UriInfo; |
26 | | -import java.util.Collections; |
27 | | -import java.util.List; |
28 | | -import java.util.Map; |
| 45 | +import java.util.*; |
| 46 | +import java.util.function.Consumer; |
| 47 | +import java.util.stream.Collectors; |
| 48 | + |
| 49 | +import static com.walmartlabs.concord.server.events.github.Constants.*; |
| 50 | + |
| 51 | +public class GithubTriggerProcessor { |
| 52 | + |
| 53 | + private static final int VERSION_ID = 2; |
| 54 | + private static final Logger log = LoggerFactory.getLogger(GithubTriggerProcessor.class); |
| 55 | + |
| 56 | + private final Dao dao; |
| 57 | + private final List<EventEnricher> eventEnrichers; |
| 58 | + private final boolean isDisableReposOnDeletedRef; |
| 59 | + |
| 60 | + @Inject |
| 61 | + public GithubTriggerProcessor(Dao dao, |
| 62 | + List<EventEnricher> eventEnrichers, |
| 63 | + GithubConfiguration githubCfg) { |
| 64 | + this.dao = dao; |
| 65 | + this.eventEnrichers = eventEnrichers; |
| 66 | + this.isDisableReposOnDeletedRef = githubCfg.isDisableReposOnDeletedRef(); |
| 67 | + } |
| 68 | + |
| 69 | + @WithTimer |
| 70 | + public void process(String eventName, Payload payload, UriInfo uriInfo, List<Result> result) { |
| 71 | + GithubKey githubKey = GithubKey.getCurrent(); |
| 72 | + UUID projectId = githubKey.getProjectId(); |
| 73 | + |
| 74 | + if (isDisableReposOnDeletedRef |
| 75 | + && PUSH_EVENT.equals(payload.eventName()) |
| 76 | + && isRefDeleted(payload)) { |
| 77 | + |
| 78 | + List<RepositoryEntry> repositories = dao.findRepos(payload.getFullRepoName()); |
| 79 | + // disable repos configured with the event branch |
| 80 | + repositories.stream() |
| 81 | + .filter(r -> !r.isDisabled() && null != r.getBranch()) |
| 82 | + .filter(r -> r.getBranch().equals(payload.getBranch())) |
| 83 | + .forEach(r -> disableRepo(r, payload)); |
| 84 | + } |
| 85 | + |
| 86 | + List<TriggerEntry> triggers = listTriggers(projectId, payload.getOrg(), payload.getRepo()); |
| 87 | + for (TriggerEntry t : triggers) { |
| 88 | + if (skipTrigger(t, eventName, payload)) { |
| 89 | + continue; |
| 90 | + } |
| 91 | + |
| 92 | + Map<String, Object> event = buildEvent(eventName, uriInfo, payload); |
| 93 | + enrichEventConditions(payload, t, event); |
| 94 | + |
| 95 | + if (DefaultEventFilter.filter(event, t)) { |
| 96 | + result.add(new Result(event, t)); |
| 97 | + } |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + static boolean skipTrigger(TriggerEntry t, String eventName, Payload payload) { |
| 102 | + // skip empty push events if the trigger's configuration says so |
| 103 | + if (GithubUtils.ignoreEmptyPush(t) && GithubUtils.isEmptyPush(eventName, payload)) { |
| 104 | + return true; |
| 105 | + } |
| 106 | + |
| 107 | + // process is destined to fail if attempted to start from commit in another repo |
| 108 | + // on an event from a pull request. |
| 109 | + if (TriggerUtils.isUseEventCommitId(t) |
| 110 | + && payload.hasPullRequestEntry() |
| 111 | + && payload.isPullRequestFromDifferentRepo()) { |
| 112 | + |
| 113 | + log.info("Skip start from {} event [{}, {}] -> Commit is in a different repository.", |
| 114 | + eventName, payload.getPullRequestBaseUrl(), payload.getPullRequestHeadUrl()); |
| 115 | + |
| 116 | + return true; |
| 117 | + } |
| 118 | + |
| 119 | + return false; |
| 120 | + } |
| 121 | + |
| 122 | + private void enrichEventConditions(Payload payload, TriggerEntry trigger, Map<String, Object> result) { |
| 123 | + for (EventEnricher e : eventEnrichers) { |
| 124 | + e.enrich(payload, trigger, result); |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + private void disableRepo(RepositoryEntry repo, Payload payload) { |
| 129 | + log.info("disable repo ['{}', '{}'] -> ref deleted", repo.getId(), payload.getBranch()); |
| 130 | + dao.disable(repo.getProjectId(), repo.getId()); |
| 131 | + } |
| 132 | + |
| 133 | + private static boolean isRefDeleted(Payload payload) { |
| 134 | + Object val = payload.raw().get("deleted"); |
| 135 | + |
| 136 | + if (val == null) { |
| 137 | + return false; |
| 138 | + } |
| 139 | + |
| 140 | + if (val instanceof String str) { |
| 141 | + return Boolean.parseBoolean(str); |
| 142 | + } |
29 | 143 |
|
30 | | -public interface GithubTriggerProcessor { |
| 144 | + return Boolean.TRUE.equals(val); |
| 145 | + } |
| 146 | + |
| 147 | + @WithTimer |
| 148 | + List<TriggerEntry> listTriggers(UUID projectId, String org, String repo) { |
| 149 | + return dao.listTriggers(projectId, org, repo); |
| 150 | + } |
| 151 | + |
| 152 | + private Map<String, Object> buildEvent(String eventName, UriInfo uriInfo, Payload payload) { |
| 153 | + Map<String, Object> result = new HashMap<>(); |
| 154 | + |
| 155 | + result.put(GITHUB_ORG_KEY, payload.getOrg()); |
| 156 | + result.put(GITHUB_REPO_KEY, payload.getRepo()); |
| 157 | + result.put(GITHUB_HOST_KEY, payload.getHost()); |
| 158 | + String branch = payload.getBranch(); |
| 159 | + if (branch != null) { |
| 160 | + result.put(REPO_BRANCH_KEY, payload.getBranch()); |
| 161 | + } |
| 162 | + |
| 163 | + if (PULL_REQUEST_EVENT.equals(eventName)) { |
| 164 | + Map<String, Object> pullRequest = MapUtils.getMap(payload.raw(), PULL_REQUEST_EVENT, Collections.emptyMap()); |
| 165 | + Map<String, Object> head = MapUtils.getMap(pullRequest, "head", Collections.emptyMap()); |
| 166 | + String sha = MapUtils.getString(head, "sha"); |
| 167 | + if (sha != null) { |
| 168 | + result.put(COMMIT_ID_KEY, sha); |
| 169 | + } |
| 170 | + } else if (PUSH_EVENT.equals(eventName)) { |
| 171 | + String after = payload.getString("after"); |
| 172 | + if (after != null) { |
| 173 | + result.put(COMMIT_ID_KEY, after); |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + result.put(SENDER_KEY, payload.getSender()); |
| 178 | + result.put(TYPE_KEY, eventName); |
| 179 | + result.put(STATUS_KEY, payload.getAction()); |
| 180 | + result.put(PAYLOAD_KEY, payload.raw()); |
| 181 | + result.put(QUERY_PARAMS_KEY, new HashMap<>(uriInfo.getQueryParameters())); |
| 182 | + |
| 183 | + // files |
| 184 | + Map<String, Set<String>> files = new HashMap<>(payload.getFiles()); |
| 185 | + // alias for all files (changed/modified/deleted) |
| 186 | + files.put("any", files.values().stream() |
| 187 | + .flatMap(Set::stream) |
| 188 | + .collect(Collectors.toSet())); |
| 189 | + result.put(FILES_KEY, files); |
| 190 | + |
| 191 | + // match only with v2 triggers |
| 192 | + result.put(VERSION_KEY, VERSION_ID); |
| 193 | + |
| 194 | + return result; |
| 195 | + } |
| 196 | + |
| 197 | + public interface EventEnricher { |
| 198 | + |
| 199 | + void enrich(Payload payload, TriggerEntry trigger, Map<String, Object> result); |
| 200 | + } |
| 201 | + |
| 202 | + /** |
| 203 | + * Adds {@link Trigger#REPOSITORY_INFO} property to the event, but only if |
| 204 | + * the trigger's conditions contained the clause with the same key. |
| 205 | + */ |
| 206 | + @Named |
| 207 | + private static class RepositoryInfoEnricher implements EventEnricher { |
31 | 208 |
|
32 | | - void process(String eventName, Payload payload, UriInfo uriInfo, List<Result> result); |
| 209 | + private final Dao dao; |
33 | 210 |
|
34 | | - class Result { |
| 211 | + @Inject |
| 212 | + public RepositoryInfoEnricher(Dao dao) { |
| 213 | + this.dao = dao; |
| 214 | + } |
| 215 | + |
| 216 | + @Override |
| 217 | + @WithTimer |
| 218 | + public void enrich(Payload payload, TriggerEntry trigger, Map<String, Object> result) { |
| 219 | + Object projectInfoConditions = trigger.getConditions().get(Trigger.REPOSITORY_INFO); |
| 220 | + if (projectInfoConditions == null || payload.getFullRepoName() == null) { |
| 221 | + return; |
| 222 | + } |
| 223 | + |
| 224 | + List<Map<String, Object>> repositoryInfos = new ArrayList<>(); |
| 225 | + List<RepositoryEntry> repositories = dao.findRepos(payload.getFullRepoName()); |
35 | 226 |
|
36 | | - private final Map<String, Object> event; |
| 227 | + for (RepositoryEntry r : repositories) { |
| 228 | + if (r.isDisabled()) { |
| 229 | + continue; |
| 230 | + } |
37 | 231 |
|
38 | | - private final List<TriggerEntry> triggers; |
| 232 | + Map<String, Object> repositoryInfo = new HashMap<>(); |
| 233 | + repositoryInfo.put(REPO_ID_KEY, r.getId()); |
| 234 | + repositoryInfo.put(REPO_NAME_KEY, r.getName()); |
| 235 | + repositoryInfo.put(PROJECT_ID_KEY, r.getProjectId()); |
| 236 | + if (r.getBranch() != null) { |
| 237 | + repositoryInfo.put(REPO_BRANCH_KEY, r.getBranch()); |
| 238 | + } |
| 239 | + repositoryInfo.put(REPO_ENABLED_KEY, !r.isDisabled()); |
39 | 240 |
|
40 | | - public Result(Map<String, Object> event, List<TriggerEntry> triggers) { |
41 | | - this.event = event; |
42 | | - this.triggers = triggers; |
| 241 | + repositoryInfos.add(repositoryInfo); |
| 242 | + } |
| 243 | + |
| 244 | + if (!repositoryInfos.isEmpty()) { |
| 245 | + result.put(Trigger.REPOSITORY_INFO, repositoryInfos); |
| 246 | + } |
43 | 247 | } |
| 248 | + } |
44 | 249 |
|
45 | | - public Map<String, Object> event() { |
46 | | - return event; |
| 250 | + @Named |
| 251 | + @Singleton |
| 252 | + public static class Dao { |
| 253 | + private final RepositoryDao repoDao; |
| 254 | + private final TriggersDao triggersDao; |
| 255 | + private final Configuration cfg; |
| 256 | + |
| 257 | + @Inject |
| 258 | + public Dao(@MainDB Configuration cfg, |
| 259 | + RepositoryDao repoDao, |
| 260 | + TriggersDao triggersDao) { |
| 261 | + this.cfg = cfg; |
| 262 | + this.triggersDao = triggersDao; |
| 263 | + this.repoDao = repoDao; |
47 | 264 | } |
48 | 265 |
|
49 | | - public List<TriggerEntry> triggers() { |
50 | | - return triggers; |
| 266 | + private List<RepositoryEntry> findRepos(String repoOrgAndName) { |
| 267 | + String sshAndHttpPattern = "%[/:]" + repoOrgAndName + "(.git)?/?"; |
| 268 | + return repoDao.findSimilar(sshAndHttpPattern); |
51 | 269 | } |
52 | 270 |
|
53 | | - static Result from(Map<String, Object> event, TriggerEntry trigger) { |
54 | | - return new Result(event, Collections.singletonList(trigger)); |
| 271 | + List<TriggerEntry> listTriggers(UUID projectId, String org, String repo) { |
| 272 | + Map<String, String> conditions = new HashMap<>(); |
| 273 | + |
| 274 | + if (org != null) { |
| 275 | + conditions.put(GITHUB_ORG_KEY, org); |
| 276 | + } |
| 277 | + |
| 278 | + if (repo != null) { |
| 279 | + conditions.put(GITHUB_REPO_KEY, repo); |
| 280 | + } |
| 281 | + |
| 282 | + return triggersDao.list(projectId, EVENT_SOURCE, VERSION_ID, conditions); |
55 | 283 | } |
56 | 284 |
|
57 | | - static Result from(Map<String, Object> event, List<TriggerEntry> triggers) { |
58 | | - return new Result(event, triggers); |
| 285 | + void disable(UUID projectId, UUID repoId) { |
| 286 | + tx(tx -> { |
| 287 | + repoDao.disable(tx, repoId); |
| 288 | + triggersDao.delete(tx, projectId, repoId); |
| 289 | + }); |
| 290 | + } |
| 291 | + |
| 292 | + private DSLContext dsl() { |
| 293 | + return DSL.using(cfg); |
| 294 | + } |
| 295 | + |
| 296 | + private void tx(Consumer<DSLContext> c) { |
| 297 | + dsl().transaction(localCfg -> { |
| 298 | + DSLContext tx = DSL.using(localCfg); |
| 299 | + c.accept(tx); |
| 300 | + }); |
| 301 | + } |
| 302 | + } |
| 303 | + |
| 304 | + public record Result(Map<String, Object> event, List<TriggerEntry> triggers) { |
| 305 | + |
| 306 | + private Result(Map<String, Object> event, TriggerEntry trigger) { |
| 307 | + this(event, List.of(trigger)); |
59 | 308 | } |
60 | 309 | } |
61 | 310 | } |
0 commit comments