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

Commit

Permalink
Auto merge pull request #3 from atomist/sdm-pack-docker
Browse files Browse the repository at this point in the history
* Add docker run deployment

* Autofix: TypeScript header

[atomist:generated] [atomist:autofix=TypeScript header]

* Make source port configurable
Add protocol to base URL default

* Process PR comments
  • Loading branch information
lievendoclo authored and atomist-bot committed Sep 13, 2018
1 parent cc29c5e commit 64a4563
Show file tree
Hide file tree
Showing 11 changed files with 464 additions and 85 deletions.
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.
113 changes: 113 additions & 0 deletions lib/docker/DockerDeploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* 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,
FulfillableGoalDetails,
FulfillableGoalWithRegistrations,
getGoalDefintionFrom,
Goal,
GoalInvocation,
ImplementationRegistration,
IndependentOfEnvironment,
} from "@atomist/sdm";
import {
DockerPerBranchDeployer,
DockerPerBranchDeployerOptions,
} from "./DockerPerBranchDeployer";

/**
* Options for Docker deployment goal
*/
export interface DockerDeployRegistration extends Partial<ImplementationRegistration> {
/**
* Starting port to be scanned for free ports. Defaults to 9090
*/
lowerPort?: number;
/**
* Pattern that indicates that the container has started up correctly
*/
successPatterns: RegExp[];
/**
* Base URL for the docker container. Probably localhost or your Docker machine IP. Defaults to localhost
*/
baseUrl?: string;
/**
* The exposed port in the Dockerfile to be mapped externally
*/
sourcePort: number;
}

/**
* Goal definition for deployment using Docker
*/
export const DockerDeployGoal = new Goal({
uniqueName: "docker-deploy",
displayName: "docker deploy",
environment: IndependentOfEnvironment,
workingDescription: "Deploying using Docker",
completedDescription: "Deployed with Docker",
failedDescription: "Docker deployment failed",
});

/**
* This goal will deploy the Docker image built by the `DockerBuild` goal. Deployments mapped
* to free ports and a deployment will be done per branch.
*/
export class DockerDeploy extends FulfillableGoalWithRegistrations<DockerDeployRegistration> {
constructor(private readonly goalDetailsOrUniqueName: FulfillableGoalDetails | string = DefaultGoalNameGenerator.generateName("docker-deploy"),
...dependsOn: Goal[]) {
super({
...DockerDeployGoal.definition,
...getGoalDefintionFrom(goalDetailsOrUniqueName, DefaultGoalNameGenerator.generateName("docker-deploy")),
displayName: "version",
}, ...dependsOn);
}

public with(registration: DockerDeployRegistration): 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"),
...registration as ImplementationRegistration,
});
return this;
}
}

function executeDockerRun(options: DockerPerBranchDeployerOptions): ExecuteGoal {
const deployer = new DockerPerBranchDeployer(options);
return async (goalInvocation: GoalInvocation): Promise<ExecuteGoalResult> => {
return await deployer.deployProject(goalInvocation).then(deployment => {
return {
code: 0,
targetUrl: deployment.endpoint,
};
},
).catch(reason => {
return {
code: 1,
message: reason,
};
});
};
}
142 changes: 142 additions & 0 deletions lib/docker/DockerPerBranchDeployer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* 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";

/**
* Options for the DockerPerBranchDeployer
*/
export interface DockerPerBranchDeployerOptions {
/**
* Starting port to be scanned for free ports
*/
lowerPort: number;
/**
* Pattern that indicates that the container has started up correctly
*/
successPatterns: RegExp[];
/**
* Base URL for the docker container. Probably localhost or your Docker machine IP
*/
baseUrl: string;
/**
* The exposed port in the Dockerfile to be mapped externally
*/
sourcePort: number;
}

/**
* Deployer that uses `docker run` in order to deploy images produces by the `DockerBuild` goal.
*/
export class DockerPerBranchDeployer {

// 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: DockerPerBranchDeployerOptions) {}

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("Docker deployment failure vvvvvvvvvvvvvvvvvvvvvv");
logger.error("stdout:\n%s\nstderr:\n%s\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^", stdout, stderr);
reject(new Error("Docker deployment failure"));
}
});
childProcess.addListener("error", reject);
});
}
}

function stopAndRemoveContainer(existingContainer: string) {
spawn("docker",
[
"rm",
"-f",
existingContainer,
]);
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
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 {
DockerDeploy,
DockerDeployRegistration,
} from "./docker/DockerDeploy";
export {
DockerProgressReporter,
DockerProgressTests,
} from "./docker/DockerProgressReporter";

0 comments on commit 64a4563

Please sign in to comment.