Skip to content
This repository has been archived by the owner on Jul 13, 2020. It is now read-only.

Support for per branch deployment using docker run #3

Merged
merged 5 commits into from
Sep 13, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/build/
/src/typings
/lib/typings
node_modules/
*.js
npm-debug.log
Expand Down
File renamed without changes.
81 changes: 81 additions & 0 deletions lib/docker/DockerRun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright © 2018 Atomist, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
DefaultGoalNameGenerator,
ExecuteGoal,
ExecuteGoalResult,
FulfillableGoalWithRegistrations,
GoalInvocation,
ImplementationRegistration,
IndependentOfEnvironment,
} from "@atomist/sdm";
import {
DockerRunner,
DockerRunnerOptions,
} from "./DockerRunner";

export interface DockerRunRegistration extends Partial<ImplementationRegistration> {
lievendoclo marked this conversation as resolved.
Show resolved Hide resolved
lowerPort?: number;
successPatterns: RegExp[];
baseUrl?: string;
sourcePort: number;
}

export class DockerRun extends FulfillableGoalWithRegistrations<DockerRunRegistration> {
lievendoclo marked this conversation as resolved.
Show resolved Hide resolved
constructor(uniqueName: string = DefaultGoalNameGenerator.generateName("docker-run")) {
lievendoclo marked this conversation as resolved.
Show resolved Hide resolved
super({
uniqueName,
displayName: "docker run",
environment: IndependentOfEnvironment,
workingDescription: "Running docker run",
completedDescription: "Docker run successful",
failedDescription: "Docker run failed",
isolated: true,
});
}

public with(registration: DockerRunRegistration): this {
this.addFulfillment({
goalExecutor: executeDockerRun( {
successPatterns: registration.successPatterns,
lowerPort: registration.lowerPort ? registration.lowerPort : 9090,
baseUrl: registration.baseUrl ? registration.baseUrl : "http://localhost",
sourcePort: registration.sourcePort,
}),
name: DefaultGoalNameGenerator.generateName("docker-runner"),
lievendoclo marked this conversation as resolved.
Show resolved Hide resolved
});
return this;
}
}

export function executeDockerRun(options: DockerRunnerOptions): ExecuteGoal {
const deployer = new DockerRunner(options);
return async (goalInvocation: GoalInvocation): Promise<ExecuteGoalResult> => {
return deployer.deployProject(goalInvocation).then(deployment => {
lievendoclo marked this conversation as resolved.
Show resolved Hide resolved
return {
code: 0,
targetUrl: deployment.endpoint,
};
},
).catch(reason => {
return {
code: 1,
message: reason,
};
});
};
}
124 changes: 124 additions & 0 deletions lib/docker/DockerRunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright © 2018 Atomist, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
GoalInvocation,
logger,
} from "@atomist/sdm";
import { SpawnedDeployment } from "@atomist/sdm-core";
import { DelimitedWriteProgressLogDecorator } from "@atomist/sdm/api-helper/log/DelimitedWriteProgressLogDecorator";
import { spawn } from "child_process";
import * as portfinder from "portfinder";

export interface DockerRunnerOptions {
lievendoclo marked this conversation as resolved.
Show resolved Hide resolved
lowerPort: number;
successPatterns: RegExp[];
baseUrl: string;
sourcePort: number;
}

export class DockerRunner {
lievendoclo marked this conversation as resolved.
Show resolved Hide resolved

// Already allocated ports
public readonly repoBranchToPort: { [repoAndBranch: string]: number } = {};

// Keys are ports: values are containerIds
private readonly portToContainer: { [port: number]: string } = {};

constructor(private readonly options: DockerRunnerOptions) {}

public async deployProject(goalInvocation: GoalInvocation): Promise<SpawnedDeployment> {
const branch = goalInvocation.sdmGoal.branch;

let port = this.repoBranchToPort[goalInvocation.id.repo + ":" + branch];
if (!port) {
port = await portfinder.getPortPromise({ /*host: this.options.baseUrl,*/ port: this.options.lowerPort });
this.repoBranchToPort[goalInvocation.id.repo + ":" + branch] = port;
}
const existingContainer = this.portToContainer[port];
if (!!existingContainer) {
await stopAndRemoveContainer(existingContainer);
} else {
// Check we won't end with a crazy number of child processes
const presentCount = Object.keys(this.portToContainer)
.filter(n => typeof n === "number")
.length;
if (presentCount >= 5) {
throw new Error(`Unable to deploy project at ${goalInvocation.id} as limit of 5 has been reached`);
}
}

const name = `${goalInvocation.id.repo}_${branch}`;
const childProcess = spawn("docker",
[
"run",
`-p${port}:${this.options.sourcePort}`,
`--name=${name}`,
goalInvocation.sdmGoal.push.after.image.imageName,
],
{});
if (!childProcess.pid) {
throw new Error("Fatal error deploying using Docker");
}
const deployment = {
childProcess,
endpoint: `${this.options.baseUrl}:${port}`,
};

this.portToContainer[port] = name;

const newLineDelimitedLog = new DelimitedWriteProgressLogDecorator(goalInvocation.progressLog, "\n");
childProcess.stdout.on("data", what => newLineDelimitedLog.write(what.toString()));
childProcess.stderr.on("data", what => newLineDelimitedLog.write(what.toString()));
let stdout = "";
let stderr = "";

return new Promise<SpawnedDeployment>((resolve, reject) => {
childProcess.stdout.addListener("data", what => {
if (!!what) {
stdout += what.toString();
}
if (this.options.successPatterns.some(successPattern => successPattern.test(stdout))) {
resolve(deployment);
}
});
childProcess.stderr.addListener("data", what => {
if (!!what) {
stderr += what.toString();
}
});
childProcess.addListener("exit", async () => {
if (this.options.successPatterns.some(successPattern => successPattern.test(stdout))) {
resolve(deployment);
} else {
logger.error("Maven deployment failure vvvvvvvvvvvvvvvvvvvvvv");
lievendoclo marked this conversation as resolved.
Show resolved Hide resolved
logger.error("stdout:\n%s\nstderr:\n%s\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^", stdout, stderr);
reject(new Error("Maven deployment failure"));
lievendoclo marked this conversation as resolved.
Show resolved Hide resolved
}
});
childProcess.addListener("error", reject);
});
}
}

function stopAndRemoveContainer(existingContainer: string) {
spawn("docker",
[
"rm",
"-f",
existingContainer,
]);
}
34 changes: 34 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright © 2018 Atomist, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export { HasDockerfile } from "./docker/dockerPushTests";
export {
DockerOptions,
DockerImageNameCreator,
executeDockerBuild,
} from "./docker/executeDockerBuild";
export {
DockerBuild,
DockerBuildRegistration,
} from "./docker/DockerBuild";
export {
DockerRun,
DockerRunRegistration,
} from "./docker/DockerRun";
export {
DockerProgressReporter,
DockerProgressTests,
} from "./docker/DockerProgressReporter";
34 changes: 28 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"url": "https://github.com/atomist/sdm-pack-docker/issues"
},
"dependencies": {
"lodash": "^4.17.10"
"lodash": "^4.17.10",
"portfinder": "^1.0.17"
},
"peerDependencies": {
"@atomist/automation-client": "*",
Expand Down
14 changes: 0 additions & 14 deletions src/index.ts

This file was deleted.