Skip to content

Commit

Permalink
feat: add new start command
Browse files Browse the repository at this point in the history
The start command allows you to run the Atlassian host product within the context of the plugin Maven project with file watchers to (re)-build your project
  • Loading branch information
remie committed Apr 16, 2024
1 parent cdd2ec8 commit e68d814
Show file tree
Hide file tree
Showing 23 changed files with 387 additions and 69 deletions.
15 changes: 12 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@types/js-yaml": "4",
"@types/node": "18.16.0",
"@types/pg": "8",
"@types/yargs": "17.0.32",
"@typescript-eslint/eslint-plugin": "7.6.0",
"@typescript-eslint/parser": "7.6.0",
"eslint": "9.0.0",
Expand All @@ -61,6 +62,7 @@
"dependencies": {
"@xmldom/xmldom": "0.8.10",
"axios": "1.6.8",
"chokidar": "3.6.0",
"commander": "12.0.0",
"docker-compose": "0.24.8",
"exit-hook": "4.0.0",
Expand All @@ -71,12 +73,19 @@
"sequelize": "6.37.2",
"simple-git": "3.24.0",
"tedious": "18.1.0",
"xpath": "0.0.34"
"xpath": "0.0.34",
"yargs": "17.7.2"
},
"release": {
"branches": [
{ "name": "main" },
{ "name": "next", "channel": "next", "prerelease": true }
{
"name": "main"
},
{
"name": "next",
"channel": "next",
"prerelease": true
}
]
}
}
178 changes: 149 additions & 29 deletions src/applications/amps.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,179 @@

import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
import { ChildProcess, spawn } from 'child_process';
import { XMLParser } from 'fast-xml-parser';
import { existsSync, readFileSync } from 'fs'
import { cwd } from 'process';
import xpath from 'xpath';
import { hideBin } from 'yargs/helpers';
import yargs from 'yargs/yargs';

import { SupportedApplications } from '../types/SupportedApplications';

const { P, activeProfiles } = yargs(hideBin(process.argv)).parseSync();
const profile = P as string || activeProfiles as string || undefined;

export class AMPS {

private static maven: ChildProcess|null;

// ------------------------------------------------------------------------------------------ Public Static Methods

public static stop() {
if (AMPS.maven) {
AMPS.maven.kill(0);
}
}

public static async build(args: Array<string>) {
return new Promise<void>((resolve, reject) => {
if (AMPS.maven) {
const killed = AMPS.maven.kill(0);
if (!killed) {
reject(new Error('Failed to terminate existing Maven process'));
}
}

AMPS.maven = spawn(
'mvn',
[ 'package', ...args ],
{ cwd: cwd(), stdio: 'inherit' }
);

AMPS.maven.on('exit', (code) => {
AMPS.maven = null;
if (code === 0 || code === 130) {
resolve();
} else {
reject(new Error(`Maven exited with code ${code}`));
}
});
});
}

public static isAtlassianPlugin = (): boolean => {
try {
const hasPomFile = existsSync('./pom.xml');
if (hasPomFile) {
const content = readFileSync('./pom.xml', 'utf8');
const parser = new XMLParser();
const pom = parser.parse(content);
return pom?.project?.packaging === 'atlassian-plugin';
}
return false;
const nodes = AMPS.getNodes('//*[local-name()=\'packaging\']');
return nodes.some(item => item.textContent === 'atlassian-plugin');
} catch (err) {
console.log(err);
return false;
}
}

public static getApplicationVersion(): string|undefined {
const node = !profile
? AMPS.getNodes('//*[local-name()=\'groupId\' and text()=\'com.atlassian.maven.plugins\']', true)
: AMPS.getNodes(`//*[local-name()='profile']/*[local-name()='id' and text()='${profile}']/..//*[local-name()='groupId' and text()='com.atlassian.maven.plugins']`, true);

if (node) {
const parentNode = node.parentNode;
if (parentNode) {
const { plugin } = AMPS.toObject(parentNode);
const version = plugin?.configuration?.productVersion;
return version ? this.doPropertyReplacement(version) : undefined;
}
}
return undefined;
}

public static getApplication(): SupportedApplications|null {
const applications = AMPS.getApplications();
return applications.length === 1 ? applications[0] : null;
if (applications.length === 1) {
return applications[0];
} else if (profile) {
const profileApplications = AMPS.getApplications(profile);
if (profileApplications.length === 1) {
return profileApplications[0];
}
}
return null;
}

public static getApplications(): Array<SupportedApplications> {
public static getApplications(profile?: string): Array<SupportedApplications> {
const result = new Set<SupportedApplications>();
if (AMPS.isAtlassianPlugin()) {
const nodes = !profile
? AMPS.getNodes('//*[local-name()=\'groupId\' and text()=\'com.atlassian.maven.plugins\']')
: AMPS.getNodes(`//*[local-name()='profile']/*[local-name()='id' and text()='${profile}']/..//*[local-name()='groupId' and text()='com.atlassian.maven.plugins']`);

nodes.forEach(node => {
const parentNode = node.parentNode;
if (parentNode) {
const { plugin } = AMPS.toObject(parentNode);
if (plugin?.artifactId?.includes(SupportedApplications.JIRA)) {
result.add(SupportedApplications.JIRA);
} else if (plugin?.artifactId?.includes(SupportedApplications.CONFLUENCE)) {
result.add(SupportedApplications.CONFLUENCE);
} else if (plugin?.artifactId?.includes(SupportedApplications.BAMBOO)) {
result.add(SupportedApplications.BAMBOO);
} else if (plugin?.artifactId?.includes(SupportedApplications.BITBUCKET)) {
result.add(SupportedApplications.BITBUCKET);
}
}
});

return Array.from(result);
}

// ------------------------------------------------------------------------------------------ Private Static Methods

private static doPropertyReplacement(value: string) {
let result = value;

// If there is a profile, replace profile properties first as they take precedence
const profileProperties = profile ? AMPS.getProperties(profile) : {};
Object.entries(profileProperties).forEach(([propertyKey, propertyValue]) => {
result = result.replaceAll(`$\{${propertyKey}}`, propertyValue);
});

const properties = AMPS.getProperties();
Object.entries(properties).forEach(([propertyKey, propertyValue]) => {
result = result.replaceAll(`$\{${propertyKey}}`, propertyValue);
});

return result;
}

private static getProperties(profile?: string): Record<string, string> {
const result: Record<string, string> = {};

const nodes = !profile
? AMPS.getNodes('//*[local-name()=\'properties\']')
: AMPS.getNodes(`//*[local-name()='profile']/*[local-name()='id' and text()='${profile}']/..//*[local-name()='properties']`);

nodes.forEach(node => {
const { properties } = AMPS.toObject(node);
Object.entries(properties as Record<string, string>).forEach(([ key, value ]) => result[key] = value);
});
return result;
}

private static getNodes(expression: string): Array<Node>;
private static getNodes(expression: string, single: true): Node|null;
private static getNodes(expression: string, single?: true): Array<Node>|Node|null {
const hasPomFile = existsSync('./pom.xml');
if (hasPomFile) {
const xml = readFileSync('./pom.xml', 'utf8');
const doc = new DOMParser().parseFromString(xml, 'text/xml');
const nodes = xpath.select('//*[local-name()=\'groupId\' and text()=\'com.atlassian.maven.plugins\']', doc);
const nodes = single ? xpath.select(expression, doc, true) : xpath.select(expression, doc, false);
if (Array.isArray(nodes)) {
nodes.forEach(node => {
const parentNode = node.parentNode;
if (parentNode) {
const parser = new XMLParser();
const { plugin } = parser.parse(new XMLSerializer().serializeToString(parentNode));
if (plugin?.artifactId?.includes(SupportedApplications.JIRA)) {
result.add(SupportedApplications.JIRA);
} else if (plugin?.artifactId?.includes(SupportedApplications.CONFLUENCE)) {
result.add(SupportedApplications.CONFLUENCE);
} else if (plugin?.artifactId?.includes(SupportedApplications.BAMBOO)) {
result.add(SupportedApplications.BAMBOO);
} else if (plugin?.artifactId?.includes(SupportedApplications.BITBUCKET)) {
result.add(SupportedApplications.BITBUCKET);
}
}
});
return nodes;
} else if (single) {
return nodes as Node;
} else {
return [];
}
}
return Array.from(result);
return [];
}

private static toObject(node: Node) {
try {
const parser = new XMLParser();
return parser.parse(new XMLSerializer().serializeToString(node));
} catch (err) {
return null;
}
}

}
10 changes: 7 additions & 3 deletions src/applications/bamboo.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@

import axios from 'axios';

import { getFullPath } from '../helpers/assets';
import { timebomb } from '../helpers/licences';
import { toAbsolutePath } from '../helpers/toAbsolutePath';
import { ApplicationOptions } from '../types/ApplicationOptions';
import { DatabaseEngine } from '../types/DatabaseEngine';
import { Service } from '../types/DockerComposeV3';
Expand Down Expand Up @@ -31,11 +31,15 @@ export class Bamboo extends Base {

return {
build: {
context: getFullPath('../../assets'),
context: toAbsolutePath('../../assets'),
dockerfile_inline: `
FROM dcdx/${this.name}:${this.options.version}
COPY ./quickreload-5.0.2.jar /var/atlassian/application-data/bamboo/shared/plugins/quickreload-5.0.2.jar
COPY ./mysql-connector-j-8.3.0.jar /opt/atlassian/bamboo/lib/mysql-connector-j-8.3.0.jar
COPY ./quickreload-5.0.2.jar /var/atlassian/application-data/bamboo/shared/plugins/quickreload-5.0.2.jar
RUN echo "/opt/quickreload" > /var/atlassian/application-data/bamboo/quickreload.properties; \
mkdir -p /opt/quickreload; \
chown -R bamboo:bamboo /opt/quickreload;
RUN chown -R bamboo:bamboo /var/atlassian/application-data/bamboo`
},
ports: [
Expand Down
25 changes: 17 additions & 8 deletions src/applications/base.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios from 'axios';
import { spawn } from 'child_process';
import { downAll, ps, upAll } from 'docker-compose/dist/v2.js';
import { downAll, execCompose, ps, upAll } from 'docker-compose/dist/v2.js';
import EventEmitter from 'events';
import { gracefulExit } from 'exit-hook';
import { existsSync, mkdirSync } from 'fs';
Expand Down Expand Up @@ -64,6 +64,20 @@ export abstract class Base extends EventEmitter {
await this.down();
}

async cp(filename: string) {
const service = await this.getServiceState();
const isRunning = service && service.state.toLowerCase().startsWith('up');
if (isRunning) {
const config = this.getDockerComposeConfig();
const configAsString = dump(config);
await execCompose('cp', [ filename, `${this.name}:/opt/quickreload/` ], {
cwd: cwd(),
configAsString,
log: false
});
}
}

// ------------------------------------------------------------------------------------------ Protected Methods

protected abstract getService(): Service;
Expand Down Expand Up @@ -213,10 +227,8 @@ export abstract class Base extends EventEmitter {
const docker = spawn(
'docker',
[ 'logs', '-f', '-n', '5000', service ],
{ cwd: cwd() }
{ cwd: cwd(), stdio: 'inherit' }
);
docker.stdout.on('data', (lines: Buffer) => { console.log(lines.toString('utf-8').trim()); });
docker.stderr.on('data', (lines: Buffer) => { console.log(lines.toString('utf-8').trim()); });
docker.on('exit', (code) => (code === 0) ? resolve() : reject(new Error(`Docker exited with code ${code}`)));
});
}
Expand All @@ -226,11 +238,8 @@ export abstract class Base extends EventEmitter {
const docker = spawn(
'docker',
[ 'exec', '-i', service, `tail`, `-F`, `-n`, `5000`, this.logFilePath ],
{ cwd: cwd() }
{ cwd: cwd(), stdio: 'inherit' }
);
docker.stdout.on('data', (lines: Buffer) => { console.log(lines.toString('utf-8').trim()); });
docker.stderr.on('data', (lines: Buffer) => { console.log(lines.toString('utf-8').trim()); });
docker.on('SIGINT', () => resolve());
docker.on('exit', (code) => (code === 0) ? resolve() : reject(new Error(`Docker exited with code ${code}`)));
});
}
Expand Down
7 changes: 5 additions & 2 deletions src/applications/bitbucket.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import { getFullPath } from '../helpers/assets';
import { timebomb } from '../helpers/licences';
import { toAbsolutePath } from '../helpers/toAbsolutePath';
import { ApplicationOptions } from '../types/ApplicationOptions';
import { DatabaseEngine } from '../types/DatabaseEngine';
import { Service } from '../types/DockerComposeV3';
Expand Down Expand Up @@ -29,11 +29,14 @@ export class Bitbucket extends Base {

return {
build: {
context: getFullPath('../../assets'),
context: toAbsolutePath('../../assets'),
dockerfile_inline: `
FROM dcdx/${this.name}:${this.options.version}
COPY ./quickreload-5.0.2.jar /var/atlassian/application-data/bitbucket/plugins/installed-plugins/quickreload-5.0.2.jar
COPY ./mysql-connector-j-8.3.0.jar /var/atlassian/application-data/bitbucket/lib/mysql-connector-j-8.3.0.jar
RUN echo "/opt/quickreload" > /var/atlassian/application-data/bitbucket/quickreload.properties; \
mkdir -p /opt/quickreload; \
chown -R bitbucket:bitbucket /opt/quickreload;
RUN mkdir -p /var/atlassian/application-data/bitbucket/shared; \
touch /var/atlassian/application-data/bitbucket/shared/bitbucket.properties; \
Expand Down
10 changes: 7 additions & 3 deletions src/applications/confluence.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import { getFullPath } from '../helpers/assets';
import { timebomb } from '../helpers/licences';
import { toAbsolutePath } from '../helpers/toAbsolutePath';
import { ApplicationOptions } from '../types/ApplicationOptions';
import { DatabaseEngine } from '../types/DatabaseEngine';
import { Service } from '../types/DockerComposeV3';
Expand Down Expand Up @@ -29,11 +29,15 @@ export class Confluence extends Base {

return {
build: {
context: getFullPath('../../assets'),
context: toAbsolutePath('../../assets'),
dockerfile_inline: `
FROM dcdx/${this.name}:${this.options.version}
COPY ./quickreload-5.0.2.jar /opt/atlassian/confluence/confluence/WEB-INF/atlassian-bundled-plugins/quickreload-5.0.2.jar
COPY ./mysql-connector-j-8.3.0.jar /opt/atlassian/confluence/confluence/WEB-INF/lib/mysql-connector-j-8.3.0.jar
COPY ./quickreload-5.0.2.jar /opt/atlassian/confluence/confluence/WEB-INF/atlassian-bundled-plugins/quickreload-5.0.2.jar
RUN echo "/opt/quickreload" > /var/atlassian/application-data/confluence/quickreload.properties; \
mkdir -p /opt/quickreload; \
chown -R confluence:confluence /opt/quickreload;
RUN chown -R confluence:confluence /opt/atlassian/confluence`
},
ports: [
Expand Down
Loading

0 comments on commit e68d814

Please sign in to comment.