-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(nebula): add support for nebula gradle-metrics-plugin
- Loading branch information
Showing
10 changed files
with
469 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export enum NebulaConfig { | ||
ENABLED = "nebula:enabled", | ||
SECRET = "nebula:secret", | ||
CONTEXT = "nebula:context", | ||
LOG_WEBHOOK_EVENTS = "nebula:debug" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { RepositorySourceConfigurable } from "../core/event/event-model"; | ||
import { Swingletree } from "../core/model"; | ||
import { NebulaModel } from "./model"; | ||
|
||
export namespace NebulaEvents { | ||
export enum EventType { | ||
REPORT_RECEIVED = "nebula:report-received" | ||
} | ||
|
||
abstract class NebulaEvent extends RepositorySourceConfigurable { | ||
constructor(eventType: EventType, source: Swingletree.ScmSource) { | ||
super(eventType, source); | ||
} | ||
} | ||
|
||
export class ReportReceivedEvent extends NebulaEvent { | ||
report: NebulaModel.Report; | ||
|
||
constructor(report: any, source: Swingletree.ScmSource) { | ||
super(EventType.REPORT_RECEIVED, source); | ||
|
||
this.report = report; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
export namespace NebulaModel { | ||
export enum ResultValue { | ||
UNKNOWN = "unknown", | ||
SUCCESS = "success", | ||
FAILURE = "failure", | ||
SKIPPED = "skipped" | ||
} | ||
|
||
interface KeyValue { | ||
key: string; | ||
value: string; | ||
} | ||
|
||
interface Result { | ||
status: ResultValue; | ||
} | ||
|
||
interface Event { | ||
description: string; | ||
type: string; | ||
elapsedTime: number; | ||
} | ||
|
||
interface Project { | ||
name: string; | ||
version: string; | ||
} | ||
|
||
interface Task { | ||
description: string; | ||
result: Result; | ||
startTime: string; | ||
elapsedTime: number; | ||
} | ||
|
||
interface Test { | ||
methodName: string; | ||
className: string; | ||
suiteName: string; | ||
result: Result; | ||
startTime: string; | ||
elapsedTime: number; | ||
} | ||
|
||
interface GradleBuild { | ||
version: string; | ||
parameters: Object; | ||
excludedTaskNames: string[]; | ||
buildProjectDependencies: boolean; | ||
currentDir: string; | ||
searchUpwards: boolean; | ||
projectProperties: Object[]; | ||
dryRun: boolean; | ||
rerunTasks: boolean; | ||
profile: boolean; | ||
continueOnFailure: boolean; | ||
offline: boolean; | ||
refreshDependencies: boolean; | ||
recompileScripts: boolean; | ||
parallelThreadCount: number; | ||
configureOnDemand: boolean; | ||
} | ||
|
||
interface BuildInfo { | ||
type: "gradle" | string; | ||
gradle: GradleBuild; | ||
} | ||
|
||
interface Scm { | ||
type: string; | ||
} | ||
|
||
interface Ci { | ||
type: string; | ||
} | ||
|
||
interface Info { | ||
build: BuildInfo; | ||
scm: Scm; | ||
ci: Ci; | ||
environmentVariables: KeyValue[]; | ||
systemProperties: KeyValue[]; | ||
javaVersion: string; | ||
detailedJavaVersion: string; | ||
} | ||
|
||
export interface BuildMetrics { | ||
buildId: string; | ||
project: Project; | ||
events: Event[]; | ||
tasks: Task[]; | ||
tests: Test[]; | ||
info: Info; | ||
result: Result; | ||
startTime: string; | ||
elapsedTime: string; | ||
testCount: number; | ||
eventsCount: number; | ||
eventsElapsedTime: number; | ||
tasksElapsedTime: number; | ||
testElapsedTime: number; | ||
finishedTime: string; | ||
taskCount: number; | ||
} | ||
|
||
export interface Report { | ||
eventName: "build_metrics" | "build_logs" | string; | ||
payload: { | ||
buildId: string; | ||
build: BuildMetrics; | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { TemplateEngine } from "../core/template/template-engine"; | ||
|
||
import container from "../ioc-config"; | ||
import { SwingletreeComponent } from "../component"; | ||
import { WebServer } from "../core/webserver"; | ||
import { ConfigurationService } from "../configuration"; | ||
import { NebulaConfig } from "./config"; | ||
import NebulaWebhook from "./webhook"; | ||
import NebulaStatusEmitter from "./status-emitter"; | ||
|
||
|
||
export class NebulaPlugin extends SwingletreeComponent.Component { | ||
private enabled: boolean; | ||
|
||
constructor() { | ||
super("nebula"); | ||
|
||
const configService = container.get<ConfigurationService>(ConfigurationService); | ||
this.enabled = configService.getBoolean(NebulaConfig.ENABLED); | ||
} | ||
|
||
public run(): void { | ||
const webserver = container.get<WebServer>(WebServer); | ||
|
||
// register services to dependency injection | ||
container.bind<NebulaWebhook>(NebulaWebhook).toSelf().inSingletonScope(); | ||
container.bind<NebulaStatusEmitter>(NebulaStatusEmitter).toSelf().inSingletonScope(); | ||
|
||
// initialize Emitters | ||
container.get<NebulaStatusEmitter>(NebulaStatusEmitter); | ||
|
||
// add webhook endpoint | ||
webserver.addRouter("/webhook/nebula", container.get<NebulaWebhook>(NebulaWebhook).getRoute()); | ||
|
||
} | ||
|
||
public isEnabled(): boolean { | ||
return this.enabled; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { inject, injectable } from "inversify"; | ||
import { ConfigurationService } from "../configuration"; | ||
import { NebulaConfig } from "./config"; | ||
import EventBus from "../core/event/event-bus"; | ||
import { NotificationEvent } from "../core/event/event-model"; | ||
import { Swingletree } from "../core/model"; | ||
import { TemplateEngine, Templates } from "../core/template/template-engine"; | ||
import { NebulaEvents } from "./events"; | ||
import { NebulaModel } from "./model"; | ||
|
||
|
||
@injectable() | ||
export class NebulaStatusEmitter { | ||
private readonly eventBus: EventBus; | ||
private readonly templateEngine: TemplateEngine; | ||
private readonly context: string; | ||
|
||
constructor( | ||
@inject(EventBus) eventBus: EventBus, | ||
@inject(ConfigurationService) configurationService: ConfigurationService, | ||
@inject(TemplateEngine) templateEngine: TemplateEngine | ||
) { | ||
this.eventBus = eventBus; | ||
this.templateEngine = templateEngine; | ||
this.context = configurationService.get(NebulaConfig.CONTEXT); | ||
|
||
eventBus.register(NebulaEvents.EventType.REPORT_RECEIVED, this.reportReceivedHandler, this); | ||
} | ||
|
||
public getAnnotations(report: NebulaModel.Report): Swingletree.Annotation[] { | ||
const annotations: Swingletree.Annotation[] = []; | ||
|
||
// TODO: implement | ||
|
||
return annotations; | ||
} | ||
|
||
public reportReceivedHandler(event: NebulaEvents.ReportReceivedEvent) { | ||
const annotations = this.getAnnotations(event.report); | ||
|
||
const notificationData: Swingletree.AnalysisReport = { | ||
sender: this.context, | ||
source: event.source, | ||
checkStatus: event.report.payload.build.result.status == NebulaModel.ResultValue.SUCCESS ? Swingletree.Conclusion.PASSED : Swingletree.Conclusion.BLOCKED, | ||
title: `Gradle Build`, | ||
metadata: { | ||
}, | ||
annotations: annotations | ||
}; | ||
|
||
const notificationEvent = new NotificationEvent(notificationData); | ||
this.eventBus.emit(notificationEvent); | ||
} | ||
} | ||
|
||
export default NebulaStatusEmitter; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
"use strict"; | ||
|
||
import { Router, Request, Response, NextFunction } from "express"; | ||
import { injectable } from "inversify"; | ||
import { inject } from "inversify"; | ||
import EventBus from "../core/event/event-bus"; | ||
import { ConfigurationService } from "../configuration"; | ||
import * as BasicAuth from "basic-auth"; | ||
import { LOGGER } from "../logger"; | ||
import { Swingletree } from "../core/model"; | ||
import { NebulaConfig } from "./config"; | ||
import { NebulaEvents } from "./events"; | ||
import InstallationStorage from "../core/github/client/installation-storage"; | ||
import { NebulaModel } from "./model"; | ||
|
||
|
||
/** Provides a Webhook for Sonar | ||
*/ | ||
@injectable() | ||
export class NebulaWebhook { | ||
private eventBus: EventBus; | ||
private configurationService: ConfigurationService; | ||
private installationStorage: InstallationStorage; | ||
|
||
constructor( | ||
@inject(EventBus) eventBus: EventBus, | ||
@inject(ConfigurationService) configurationService: ConfigurationService, | ||
@inject(InstallationStorage) installationStorage: InstallationStorage | ||
) { | ||
this.eventBus = eventBus; | ||
this.configurationService = configurationService; | ||
this.installationStorage = installationStorage; | ||
} | ||
|
||
private isWebhookEventRelevant(event: NebulaModel.Report) { | ||
return event.payload.build.result.status != NebulaModel.ResultValue.UNKNOWN; | ||
} | ||
|
||
private authenticationMiddleware(secret: string) { | ||
return (req: Request, res: Response, next: NextFunction) => { | ||
const auth = BasicAuth(req); | ||
if (auth && secret === auth.pass) { | ||
next(); | ||
} else { | ||
res.sendStatus(401); | ||
} | ||
}; | ||
} | ||
|
||
public getRoute(): Router { | ||
const router = Router(); | ||
const secret = this.configurationService.get(NebulaConfig.SECRET); | ||
|
||
if (secret && secret.trim().length > 0) { | ||
router.use(this.authenticationMiddleware(secret)); | ||
} else { | ||
LOGGER.warn("gradle-metrics webhook is not protected. Consider setting a gradle-metrics secret in the Swingletree configuration."); | ||
} | ||
router.post("/", this.webhook); | ||
|
||
return router; | ||
} | ||
|
||
public webhook = async (req: Request, res: Response) => { | ||
LOGGER.debug("received gradle-metrics webhook event"); | ||
|
||
const org = req.header("X-swingletree-org"); | ||
const repo = req.header("X-swingletree-repo"); | ||
const sha = req.header("X-swingletree-sha"); | ||
const branch = req.header("X-swingletree-branch"); | ||
|
||
if (this.configurationService.getBoolean(NebulaConfig.LOG_WEBHOOK_EVENTS)) { | ||
LOGGER.debug(JSON.stringify(req.body)); | ||
} | ||
|
||
try { | ||
req.body.payload.build = JSON.parse(req.body.payload.build); | ||
} catch (err) { | ||
LOGGER.warn("failed to parse gradle-metrics build payload. Skipping event."); | ||
return; | ||
} | ||
|
||
const webhookData: NebulaModel.Report = req.body; | ||
|
||
if (org == null || repo == null || sha == null || branch == null) { | ||
res.status(400).send("missing at least one of following http headers: X-swingletree-org, X-swingletree-repo, X-swingletree-sha, X-swingletree-branch"); | ||
return; | ||
} | ||
|
||
const source = new Swingletree.GithubSource(); | ||
source.owner = org; | ||
source.repo = repo; | ||
source.branch = [ branch ]; | ||
source.sha = sha; | ||
|
||
if (this.isWebhookEventRelevant(webhookData)) { | ||
const reportReceivedEvent = new NebulaEvents.ReportReceivedEvent(webhookData, source); | ||
|
||
// check if installation is available | ||
if (await this.installationStorage.getInstallationId(org)) { | ||
this.eventBus.emit(reportReceivedEvent); | ||
} else { | ||
LOGGER.info("ignored gradle metrics report for %s/%s. Swingletree may not be installed in this organization.", org, repo); | ||
} | ||
} else { | ||
LOGGER.debug("gradle-metrics webhook data did not contain a report. This event will be ignored."); | ||
res.status(400).send("gradle-metrics webhook data did not contain a report. This event will be ignored."); | ||
return; | ||
} | ||
|
||
res.status(204).send(); | ||
} | ||
} | ||
|
||
export default NebulaWebhook; |
Oops, something went wrong.