Skip to content

Commit

Permalink
feat(nebula): add support for nebula gradle-metrics-plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
error418 committed Sep 23, 2019
1 parent 27fe16f commit 7dc5518
Show file tree
Hide file tree
Showing 10 changed files with 469 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class ConfigurationService {
.env({
lowerCase: true,
separator: "_",
match: /((ST|GITHUB|SONAR|TWISTLOCK|ZAP|STORAGE|ELASTIC|LOG)_.*)|(PORT)$/i
match: /((ST|GITHUB|SONAR|TWISTLOCK|ZAP|NEBULA|STORAGE|ELASTIC|LOG)_.*)|(PORT)$/i
})
.file({
file: file,
Expand Down
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import { SwingletreeComponent } from "./component";
import { SonarQubePlugin } from "./sonar/sonar";
import { ZapPlugin } from "./zap/zap";
import { TwistlockPlugin } from "./twistlock/twistlock";
import { NebulaPlugin } from "./nebula/nebula";

// initialize dangling event handlers
container.get<CommitStatusSender>(CommitStatusSender);
container.get<GhAppInstallationHandler>(GhAppInstallationHandler);

const registry = new SwingletreeComponent.Registry([
SwingletreeCore,
NebulaPlugin,
SonarQubePlugin,
TwistlockPlugin,
ZapPlugin
Expand Down
6 changes: 6 additions & 0 deletions src/nebula/config.ts
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"
}
25 changes: 25 additions & 0 deletions src/nebula/events.ts
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;
}
}
}
113 changes: 113 additions & 0 deletions src/nebula/model.ts
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;
};
}
}
40 changes: 40 additions & 0 deletions src/nebula/nebula.ts
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;
}
}
56 changes: 56 additions & 0 deletions src/nebula/status-emitter.ts
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;
115 changes: 115 additions & 0 deletions src/nebula/webhook.ts
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;
Loading

0 comments on commit 7dc5518

Please sign in to comment.