@ossy/deployment-tools 0.0.25 → 0.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +55835 -18407
- package/dist/licenses.txt +2 -1240
- package/package.json +8 -4
- package/src/aws-credentials-client.ts +32 -0
- package/src/{caddy-client.js → caddy-client.ts} +26 -19
- package/src/ci-rest-api.ts +25 -0
- package/src/cli-commands/deploy-handler.ts +32 -0
- package/src/cli-commands/index.ts +23 -0
- package/src/cli-commands/start-handler.ts +24 -0
- package/src/cli-commands/status-handler.ts +7 -0
- package/src/cli-commands/stop-handler.ts +7 -0
- package/src/deployment-platform-client.ts +140 -0
- package/src/deployment-queue-client.ts +73 -0
- package/src/docker-client.ts +80 -0
- package/src/index.ts +7 -0
- package/src/log/index.ts +4 -0
- package/src/types/index.ts +65 -0
- package/tsconfig.json +9 -0
- package/src/aws-credentials-client.js +0 -24
- package/src/config.js +0 -22
- package/src/container-manager-client.js +0 -115
- package/src/container-manager-server.js +0 -91
- package/src/deployment-queue-client.js +0 -51
- package/src/docker-client.js +0 -80
- package/src/index.js +0 -79
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/deployment-tools",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"description": "Collection of scripts and tools to aid deployment of containers and static files",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
|
-
"build": "npx --yes @vercel/ncc build src/index.
|
|
10
|
-
"build:esbuild": "npx --yes esbuild src/index.
|
|
9
|
+
"build": "npx --yes @vercel/ncc build src/index.ts --out dist --license licenses.txt",
|
|
10
|
+
"build:esbuild": "npx --yes esbuild src/index.ts --platform=node --bundle --outfile=dist/index.js"
|
|
11
11
|
},
|
|
12
12
|
"author": "Ossy",
|
|
13
13
|
"license": "ISC",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"arg": "^5.0.2",
|
|
20
20
|
"express": "^4.18.1",
|
|
21
21
|
"nanoid": "^4.0.0",
|
|
22
|
-
"node-fetch": "^3.2.6"
|
|
22
|
+
"node-fetch": "^3.2.6",
|
|
23
|
+
"typescript": "^4.8.4"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^18.11.0"
|
|
23
27
|
}
|
|
24
28
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as core from '@actions/core'
|
|
2
|
+
import { STSClient, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts'
|
|
3
|
+
import { DeploymentPlatform } from 'types'
|
|
4
|
+
|
|
5
|
+
export class AwsCredentialsClient {
|
|
6
|
+
|
|
7
|
+
static resolveAwsCredentials(deploymentPlatform: DeploymentPlatform) {
|
|
8
|
+
// If awsRoleToAssume is present, then we assume we run in a github workflow
|
|
9
|
+
// If awsRoleToAssume is not present, then we assume they are resolved localy by aws-sdk
|
|
10
|
+
return deploymentPlatform.awsRoleToAssume
|
|
11
|
+
? AwsCredentialsClient.getTemporaryCredentials(deploymentPlatform)
|
|
12
|
+
: Promise.resolve(undefined)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static getTemporaryCredentials({ awsAccountId, awsRegion, awsRoleToAssume }: DeploymentPlatform) {
|
|
16
|
+
const stsClient = new STSClient({ region: awsRegion })
|
|
17
|
+
|
|
18
|
+
return core.getIDToken('sts.amazonaws.com')
|
|
19
|
+
.then(webIdentityToken => stsClient.send(new AssumeRoleWithWebIdentityCommand({
|
|
20
|
+
RoleArn: `arn:aws:iam::${awsAccountId}:role/${awsRoleToAssume}`,
|
|
21
|
+
RoleSessionName: 'GitHubActions',
|
|
22
|
+
DurationSeconds: 15 * 60,
|
|
23
|
+
WebIdentityToken: webIdentityToken
|
|
24
|
+
})))
|
|
25
|
+
.then(responseData => ({
|
|
26
|
+
accessKeyId: responseData.Credentials.AccessKeyId,
|
|
27
|
+
secretAccessKey: responseData.Credentials.SecretAccessKey,
|
|
28
|
+
sessionToken: responseData.Credentials.SessionToken
|
|
29
|
+
}))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fetch from 'node-fetch'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
const config = resolveConfiguration()
|
|
2
|
+
import { log } from './log'
|
|
3
|
+
import { DeploymentPlatform, ContainerDeploymentRequest } from 'types'
|
|
5
4
|
|
|
6
5
|
export const Matchers = {
|
|
7
6
|
host: host => ({ host: [host] }),
|
|
@@ -13,7 +12,7 @@ const Handlers = {
|
|
|
13
12
|
handler: 'reverse_proxy',
|
|
14
13
|
upstreams: [{ dial: `localhost:${upstreamPort}` }]
|
|
15
14
|
}),
|
|
16
|
-
subroute: routes => (
|
|
15
|
+
subroute: routes => ({
|
|
17
16
|
handler: 'subroute',
|
|
18
17
|
routes
|
|
19
18
|
})
|
|
@@ -21,28 +20,30 @@ const Handlers = {
|
|
|
21
20
|
|
|
22
21
|
export class CaddyClient {
|
|
23
22
|
|
|
24
|
-
static
|
|
25
|
-
return new CaddyClient(serverName)
|
|
26
|
-
}
|
|
23
|
+
static deploy(deploymentPlatform: DeploymentPlatform, deploymentRequest: ContainerDeploymentRequest) {
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
const url = `${deploymentRequest.subdomain}.${deploymentPlatform.activeEnvironment}.${deploymentPlatform.domain}`
|
|
26
|
+
|
|
27
|
+
log({
|
|
28
|
+
type: 'info',
|
|
29
|
+
message: `[CaddyClient] Updating caddy config to route ${url} to localhost:${deploymentRequest.hostPort}`
|
|
30
|
+
})
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
return fetch(`http://localhost:2019/config/apps/http/servers/${this.serverName}/routes/0/handle`, {
|
|
32
|
+
return fetch(`http://localhost:2019/config/apps/http/servers/${deploymentPlatform.ciServerName}/routes/0/handle`, {
|
|
34
33
|
method: 'POST',
|
|
35
34
|
headers: { 'Content-Type': 'application/json' },
|
|
36
|
-
body: JSON.stringify(
|
|
35
|
+
body: JSON.stringify(Handlers.subroute([
|
|
37
36
|
{
|
|
38
|
-
match: [Matchers.host(
|
|
39
|
-
handle: [Handlers.subroute([{ handle: [Handlers.reverseProxy(
|
|
37
|
+
match: [Matchers.host(url)],
|
|
38
|
+
handle: [Handlers.subroute([{ handle: [Handlers.reverseProxy(deploymentRequest.hostPort)]}])]
|
|
40
39
|
}
|
|
41
40
|
]))
|
|
42
41
|
})
|
|
42
|
+
.catch(() => log({ type: 'error', message: `[CaddyClient] Could not update caddy config to include ${url}` }))
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
applyDefaultServerConfig() {
|
|
45
|
+
static applyDefaultServerConfig(deploymentPlatform: DeploymentPlatform) {
|
|
46
|
+
log({ type: 'info', message: '[CaddyClient] Applying default caddy config' })
|
|
46
47
|
return fetch('http://localhost:2019/load', {
|
|
47
48
|
method: 'POST',
|
|
48
49
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -50,12 +51,17 @@ export class CaddyClient {
|
|
|
50
51
|
apps: {
|
|
51
52
|
http: {
|
|
52
53
|
servers: {
|
|
53
|
-
[
|
|
54
|
+
[deploymentPlatform.ciServerName]: {
|
|
54
55
|
listen: [':80', ':443'],
|
|
55
56
|
routes: [
|
|
56
57
|
{
|
|
57
|
-
match: [Matchers.host(`*.${
|
|
58
|
-
handle: [
|
|
58
|
+
match: [Matchers.host(`*.${deploymentPlatform.activeEnvironment}.${deploymentPlatform.domain}`)],
|
|
59
|
+
handle: [Handlers.subroute([
|
|
60
|
+
{
|
|
61
|
+
match: [Matchers.host(`${deploymentPlatform.ciSubDomain}.${deploymentPlatform.activeEnvironment}.${deploymentPlatform.domain}`)],
|
|
62
|
+
handle: [Handlers.subroute([{ handle: [Handlers.reverseProxy(deploymentPlatform.ciInternalServerPort)]}])]
|
|
63
|
+
}
|
|
64
|
+
])],
|
|
59
65
|
terminal: true
|
|
60
66
|
}
|
|
61
67
|
]
|
|
@@ -99,6 +105,7 @@ export class CaddyClient {
|
|
|
99
105
|
}
|
|
100
106
|
})
|
|
101
107
|
})
|
|
108
|
+
.catch(() => log({ type: 'error', message: '[CaddyClient] Could not apply default caddy config' }))
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { DeploymentPlatform } from 'types'
|
|
3
|
+
import { log } from './log'
|
|
4
|
+
|
|
5
|
+
export class CiRestApi {
|
|
6
|
+
|
|
7
|
+
static start(deploymentPlatform: DeploymentPlatform) {
|
|
8
|
+
const server = express()
|
|
9
|
+
|
|
10
|
+
server.use(express.json())
|
|
11
|
+
|
|
12
|
+
server.get('/', (req, res) => {
|
|
13
|
+
res.redirect('/status')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
server.get('/status', (req, res) => {
|
|
17
|
+
res.send('Server is live')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
server.listen(deploymentPlatform.ciInternalServerPort, () => {
|
|
21
|
+
log({ type: 'info', message: `[ContainerManagerServer] API is live on port ${deploymentPlatform.ciInternalServerPort}`})
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import arg from 'arg'
|
|
2
|
+
import { log } from '../log'
|
|
3
|
+
import { DeploymentPlatformClient } from 'deployment-platform-client'
|
|
4
|
+
|
|
5
|
+
export const deployHandler = cliArgs => {
|
|
6
|
+
log({ type: 'info', message: 'Running deploy command' })
|
|
7
|
+
|
|
8
|
+
const parsedArgs = arg({
|
|
9
|
+
'--username': String,
|
|
10
|
+
'-u': '--username',
|
|
11
|
+
|
|
12
|
+
'--authentication': String,
|
|
13
|
+
'--a': '--authentication',
|
|
14
|
+
|
|
15
|
+
'--target-env': String,
|
|
16
|
+
'-t': '--target-env',
|
|
17
|
+
|
|
18
|
+
'--ossyfile': String,
|
|
19
|
+
'-o': '--ossyfile',
|
|
20
|
+
|
|
21
|
+
'--platforms': String,
|
|
22
|
+
'-p': '--platforms'
|
|
23
|
+
}, { argv: cliArgs })
|
|
24
|
+
|
|
25
|
+
DeploymentPlatformClient.deploy(
|
|
26
|
+
parsedArgs['--username'],
|
|
27
|
+
parsedArgs['--authentication'],
|
|
28
|
+
parsedArgs['--target-env'],
|
|
29
|
+
parsedArgs['--platforms'],
|
|
30
|
+
parsedArgs['--ossyfile']
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { log } from '../log'
|
|
2
|
+
import { startHandler } from './start-handler'
|
|
3
|
+
import { stopHandler } from './stop-handler'
|
|
4
|
+
import { statusHandler } from './status-handler'
|
|
5
|
+
import { deployHandler } from './deploy-handler'
|
|
6
|
+
|
|
7
|
+
export const runCliCommand = ({ name, args }) => {
|
|
8
|
+
|
|
9
|
+
if (!name) return log({ type: 'error', message: 'No command provided' })
|
|
10
|
+
|
|
11
|
+
const commandHandler = {
|
|
12
|
+
start: startHandler,
|
|
13
|
+
stop: stopHandler,
|
|
14
|
+
status: statusHandler,
|
|
15
|
+
deploy: deployHandler
|
|
16
|
+
}[name]
|
|
17
|
+
|
|
18
|
+
if (!commandHandler) {
|
|
19
|
+
return log({ type: 'error', message: 'Command not implemented, did you spell it correctly?' })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
commandHandler(args)
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import arg from 'arg'
|
|
2
|
+
import { platform } from 'os'
|
|
3
|
+
import { log } from '../log'
|
|
4
|
+
import { DeploymentPlatformClient } from 'deployment-platform-client'
|
|
5
|
+
|
|
6
|
+
export const startHandler = cliArgs => {
|
|
7
|
+
log({ type: 'info', message: 'Running start command' })
|
|
8
|
+
|
|
9
|
+
const Platforms = {
|
|
10
|
+
windows: 'win32',
|
|
11
|
+
mac: 'darwin'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if ([Platforms.windows].includes(platform())) {
|
|
15
|
+
return log({ type: 'error', message: 'Deployment tools do not support this os' })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const parsedArgs = arg({
|
|
19
|
+
'--platforms': String,
|
|
20
|
+
'-p': '--platforms'
|
|
21
|
+
}, { argv: cliArgs })
|
|
22
|
+
|
|
23
|
+
DeploymentPlatformClient.start(parsedArgs['--platforms'])
|
|
24
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { resolve } from 'path'
|
|
2
|
+
import { readFileSync } from 'fs'
|
|
3
|
+
import {
|
|
4
|
+
DeploymentPlatform,
|
|
5
|
+
SupportedEnvironments,
|
|
6
|
+
ContainerDeploymentRequest,
|
|
7
|
+
DeploymentTemplate,
|
|
8
|
+
ContainerDeploymentTemplate,
|
|
9
|
+
SupportedDeploymentTypes
|
|
10
|
+
} from 'types'
|
|
11
|
+
import { CaddyClient } from './caddy-client'
|
|
12
|
+
import { DockerClient } from './docker-client'
|
|
13
|
+
import { DeploymentQueueClient } from './deployment-queue-client'
|
|
14
|
+
import { CiRestApi } from './ci-rest-api'
|
|
15
|
+
import { log } from 'log'
|
|
16
|
+
|
|
17
|
+
export class DeploymentPlatformClient {
|
|
18
|
+
|
|
19
|
+
static start(pathToDeploymentPlatforms: string) {
|
|
20
|
+
DeploymentPlatformClient.getDeploymentPlatforms(pathToDeploymentPlatforms).then(([firstPlatformFound]) => {
|
|
21
|
+
|
|
22
|
+
if (!firstPlatformFound) {
|
|
23
|
+
log({ type: 'error', message: '[DeploymentPlatformClient] Could not find a deployment platform' })
|
|
24
|
+
return Promise.reject()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
CiRestApi.start(firstPlatformFound)
|
|
28
|
+
CaddyClient.applyDefaultServerConfig(firstPlatformFound)
|
|
29
|
+
|
|
30
|
+
DeploymentQueueClient.pollForDeploymentRequests(
|
|
31
|
+
firstPlatformFound,
|
|
32
|
+
(deploymentRequest: ContainerDeploymentRequest) => {
|
|
33
|
+
DockerClient.deploy(firstPlatformFound, deploymentRequest)
|
|
34
|
+
CaddyClient.deploy(firstPlatformFound, deploymentRequest)
|
|
35
|
+
return Promise.resolve()
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
})
|
|
40
|
+
.catch(() => log({ type: 'error', message: '[DeploymentPlatformClient] Could not start the deployment platform' }))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//eslint-disable-next-line max-params
|
|
44
|
+
static deploy(
|
|
45
|
+
username,
|
|
46
|
+
authentication,
|
|
47
|
+
targetEnvironment,
|
|
48
|
+
pathToDeploymentPlatforms,
|
|
49
|
+
pathToOssyFile
|
|
50
|
+
) {
|
|
51
|
+
return Promise.all([
|
|
52
|
+
DeploymentPlatformClient.getDeploymentPlatforms(pathToDeploymentPlatforms),
|
|
53
|
+
DeploymentPlatformClient.getDeploymentTemplates(pathToOssyFile)
|
|
54
|
+
])
|
|
55
|
+
.then(([platforms, deploymentTemplates]) => {
|
|
56
|
+
deploymentTemplates.map(deploymentTemplate => {
|
|
57
|
+
|
|
58
|
+
log({
|
|
59
|
+
type: 'info',
|
|
60
|
+
message: `[DeploymentPlatformClient]: Found deployment platforms [${platforms.join(', ')}]`
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const deploymentPlatform = platforms.find(platform => platform.platformName === deploymentTemplate.targetDeploymentPlatform)
|
|
64
|
+
|
|
65
|
+
if (!deploymentPlatform) {
|
|
66
|
+
log({ type: 'error', message: `[DeploymentPlatformClient] Could not find a deployment platform with the name ${deploymentTemplate.targetDeploymentPlatform}` })
|
|
67
|
+
return Promise.reject()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
|
71
|
+
|
|
72
|
+
if (deploymentTemplate.type !== SupportedDeploymentTypes.Container) {
|
|
73
|
+
log({ type: 'error', message: `[DeploymentPlatformClient] Unsupported deployment type of ${deploymentTemplate.type}` })
|
|
74
|
+
return Promise.reject()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const deploymentRequest: ContainerDeploymentRequest = {
|
|
78
|
+
...deploymentTemplate as ContainerDeploymentTemplate,
|
|
79
|
+
env: DeploymentPlatformClient.getEnvironmentVariables(targetEnvironment, deploymentTemplate),
|
|
80
|
+
username: username,
|
|
81
|
+
authentication: authentication
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return DeploymentQueueClient.sendDeploymentRequest(deploymentPlatform, deploymentRequest)
|
|
85
|
+
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
.catch(error => console.log(error))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static getDeploymentTemplates(pathToOssyFile: string): Promise<DeploymentTemplate[]> {
|
|
92
|
+
const ossyfile = JSON.parse(readFileSync(resolve(pathToOssyFile), 'utf8'))
|
|
93
|
+
return Promise.resolve(ossyfile.deployments || [])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static getDeploymentPlatforms(pathToDeploymentPlatforms: string): Promise<DeploymentPlatform[]> {
|
|
97
|
+
let deploymentPlatforms = JSON.parse(readFileSync(resolve(pathToDeploymentPlatforms), 'utf8'))
|
|
98
|
+
|
|
99
|
+
if (!Array.isArray(deploymentPlatforms)) {
|
|
100
|
+
deploymentPlatforms = [deploymentPlatforms]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return Promise.resolve((deploymentPlatforms || []).map(DeploymentPlatformClient.resolveDeploymentPlatformValues))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static getEnvironmentVariables(targetEnvironment: SupportedEnvironments, deploymentRequest: DeploymentTemplate) {
|
|
107
|
+
const envs = deploymentRequest.env || {}
|
|
108
|
+
return {
|
|
109
|
+
...(envs.shared || {}),
|
|
110
|
+
...(envs[targetEnvironment] || {})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static resolveDeploymentPlatformValues(deploymentPlatform: DeploymentPlatform): DeploymentPlatform {
|
|
115
|
+
return DeploymentPlatformClient.setCalculatedDeploymentPlatformValues(DeploymentPlatformClient.setDefaultDeploymentPlatformValues(deploymentPlatform))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static setDefaultDeploymentPlatformValues(deploymentPlatform: DeploymentPlatform): DeploymentPlatform {
|
|
119
|
+
return {
|
|
120
|
+
platformName: SupportedEnvironments.LOCAL,
|
|
121
|
+
domain: 'localhost',
|
|
122
|
+
activeEnvironment: SupportedEnvironments.LOCAL,
|
|
123
|
+
supportedDeploymentTypes: ['CONTAINER'],
|
|
124
|
+
ciSubDomain: 'ci',
|
|
125
|
+
ciInternalServerPort: 3000,
|
|
126
|
+
ciServerName: 'ci-client',
|
|
127
|
+
ciDockerNetworkName: 'deployment-tools',
|
|
128
|
+
// awsRoleToAssume: 'github-ci-role',
|
|
129
|
+
...deploymentPlatform
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static setCalculatedDeploymentPlatformValues(deploymentPlatform: DeploymentPlatform): DeploymentPlatform {
|
|
134
|
+
return {
|
|
135
|
+
awsDeploymentSqsArn: `https://sqs.${deploymentPlatform.awsRegion}.amazonaws.com/${deploymentPlatform.awsAccountId}/${deploymentPlatform.platformName}-${deploymentPlatform.activeEnvironment}`,
|
|
136
|
+
...deploymentPlatform
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SQSClient,
|
|
3
|
+
SendMessageCommand,
|
|
4
|
+
DeleteMessageCommand,
|
|
5
|
+
ReceiveMessageCommand
|
|
6
|
+
} from '@aws-sdk/client-sqs'
|
|
7
|
+
import { DeploymentPlatform, ContainerDeploymentRequest } from 'types'
|
|
8
|
+
import { AwsCredentialsClient } from 'aws-credentials-client'
|
|
9
|
+
import { log } from 'log'
|
|
10
|
+
|
|
11
|
+
export class DeploymentQueueClient {
|
|
12
|
+
|
|
13
|
+
static sendDeploymentRequest(deploymentPlatform: DeploymentPlatform, deploymentRequest: ContainerDeploymentRequest) {
|
|
14
|
+
log({ type: 'info', message: '[DeploymentQueueClient] Sending deployment request...' })
|
|
15
|
+
return DeploymentQueueClient.createAwsSqsClient(deploymentPlatform)
|
|
16
|
+
.then(sqsClient => {
|
|
17
|
+
|
|
18
|
+
const command = new SendMessageCommand({
|
|
19
|
+
QueueUrl: deploymentPlatform.awsDeploymentSqsArn,
|
|
20
|
+
MessageBody: JSON.stringify(deploymentRequest)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
return sqsClient.send(command)
|
|
24
|
+
.then(() => log({ type: 'info', message: '[DeploymentQueueClient] Deployment request sent' }))
|
|
25
|
+
.catch(() => log({ type: 'error', message: '[DeploymentQueueClient] Could not send deployment request' }))
|
|
26
|
+
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static pollForDeploymentRequests(deploymentPlatform: DeploymentPlatform, handleDeploymentRequest: (deploymentRequest: ContainerDeploymentRequest) => Promise<void>) {
|
|
32
|
+
log({ type: 'info', message: '[DeploymentQueueClient] Starting polling for deployment requests' })
|
|
33
|
+
DeploymentQueueClient.createAwsSqsClient(deploymentPlatform).then(sqsClient => {
|
|
34
|
+
const FIVE_MINUTES = 3000
|
|
35
|
+
|
|
36
|
+
setInterval(() => {
|
|
37
|
+
|
|
38
|
+
const receiveMessageCommand = new ReceiveMessageCommand({ QueueUrl: deploymentPlatform.awsDeploymentSqsArn })
|
|
39
|
+
|
|
40
|
+
sqsClient.send(receiveMessageCommand)
|
|
41
|
+
.then(data => data.Messages.map(message => {
|
|
42
|
+
|
|
43
|
+
log({ type: 'info', message: '[DeploymentQueueClient] Received deployment request' })
|
|
44
|
+
|
|
45
|
+
handleDeploymentRequest(JSON.parse(message.Body))
|
|
46
|
+
.then(() => {
|
|
47
|
+
|
|
48
|
+
const deleteMessageCommand = new DeleteMessageCommand({
|
|
49
|
+
QueueUrl: deploymentPlatform.awsDeploymentSqsArn,
|
|
50
|
+
ReceiptHandle: message.ReceiptHandle
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
sqsClient.send(deleteMessageCommand)
|
|
54
|
+
.then(() => log({ type: 'info', message: '[DeploymentQueueClient] Removing deployment request from queue' }))
|
|
55
|
+
.catch(() => log({ type: 'error', message: '[DeploymentQueueClient] Removing deployment request from queue' }))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
}))
|
|
59
|
+
.catch(() => log({ type: 'error', message: '[ContainerManagerServer] Could not handle incoming deployment request' }))
|
|
60
|
+
}, FIVE_MINUTES)
|
|
61
|
+
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static createAwsSqsClient(deploymentPlatform: DeploymentPlatform) {
|
|
66
|
+
return AwsCredentialsClient.resolveAwsCredentials(deploymentPlatform)
|
|
67
|
+
.then(awsCredentials => new SQSClient({
|
|
68
|
+
region: deploymentPlatform.awsRegion,
|
|
69
|
+
credentials: awsCredentials
|
|
70
|
+
}))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { exec } from 'child_process'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import { nanoid } from 'nanoid'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { log } from './log'
|
|
7
|
+
import { DeploymentPlatform, ContainerDeploymentRequest } from 'types'
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = path.dirname(__filename)
|
|
11
|
+
|
|
12
|
+
export class DockerClient {
|
|
13
|
+
|
|
14
|
+
static createDockerNetworkForContainerManagerServer(deploymentPlatform: DeploymentPlatform) {
|
|
15
|
+
log({ type: 'info', message: '[DockerClient] Creating docker network for comunication between containers' })
|
|
16
|
+
exec(`sudo docker network create ${deploymentPlatform.ciDockerNetworkName}`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static stopContainer(deploymentRequest: ContainerDeploymentRequest) {
|
|
20
|
+
const name = deploymentRequest.image.replaceAll('/', '_')
|
|
21
|
+
log({ type: 'info', message: `Running docker stop for image with the name of ${name}` })
|
|
22
|
+
return `sudo docker stop ${name} ||`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static startContainer(deploymentPlatform, { image, containerPort, hostPort, registry, env }: ContainerDeploymentRequest) {
|
|
26
|
+
const name = image.replaceAll('/', '_')
|
|
27
|
+
const imageUrl = !!registry ? `${registry}/${image}` : image
|
|
28
|
+
const envsAsString = Object.entries(env || {}).reduce((envs, [name, value]) => `${envs} --env ${name}=${value}`, '')
|
|
29
|
+
log({ type: 'info', message: `Running docker start for image with the name of ${name} with port mapping ${hostPort}:${containerPort} and source ${imageUrl}` })
|
|
30
|
+
return `sudo docker run -d -p ${hostPort}:${containerPort} --name=${name} --network=${deploymentPlatform.ciDockerNetworkName} --network-alias=${name} --rm ${envsAsString} ${imageUrl}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static resolveCredentials({ registry, username, authentication }: ContainerDeploymentRequest) {
|
|
34
|
+
const shouldAuthenticate = username || authentication
|
|
35
|
+
|
|
36
|
+
if (!shouldAuthenticate) {
|
|
37
|
+
log({ type: 'info', message: 'No docker credentials provided, assuming image is publicly hosted' })
|
|
38
|
+
return ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
log({ type: 'info', message: `Resolving docker credentials for ${registry}` })
|
|
42
|
+
return `sudo docker login ${registry} -u ${username} -p ${authentication}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static deploy(deploymentPlatform: DeploymentPlatform, deploymentRequest: ContainerDeploymentRequest): Promise<void> {
|
|
46
|
+
return new Promise(resolve => {
|
|
47
|
+
log({ type: 'info', message: 'Starting docker deployment sequence' })
|
|
48
|
+
|
|
49
|
+
const dockerCommandScript = `'#!/bin/bash'
|
|
50
|
+
${DockerClient.stopContainer(deploymentRequest)}
|
|
51
|
+
${DockerClient.resolveCredentials(deploymentRequest)}
|
|
52
|
+
${DockerClient.startContainer(deploymentPlatform, deploymentRequest)}`
|
|
53
|
+
|
|
54
|
+
const deploymentId = nanoid()
|
|
55
|
+
|
|
56
|
+
const FilePaths = {
|
|
57
|
+
DeploymentScript: `${__dirname}/${deploymentId}.sh`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fs.writeFileSync(FilePaths.DeploymentScript, dockerCommandScript)
|
|
61
|
+
const command = exec(`bash ${FilePaths.DeploymentScript}`)
|
|
62
|
+
|
|
63
|
+
command.stdout.on('data', data => {
|
|
64
|
+
log({ type: 'info', message: `[DockerClient]: ${data}` })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
command.stderr.on('data', (data) => {
|
|
68
|
+
log({ type: 'error', message: `[DockerClient]: ${data}` })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
command.on('close', code => {
|
|
72
|
+
log({ type: 'info', message: `[DockerClient] command exited with code ${code}` })
|
|
73
|
+
fs.unlinkSync(FilePaths.DeploymentScript)
|
|
74
|
+
resolve()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
}
|
package/src/index.ts
ADDED
package/src/log/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface DeploymentPlatformTemplate {
|
|
2
|
+
platformName: string;
|
|
3
|
+
domain: string;
|
|
4
|
+
supportedDeploymentTypes: SupportedDeploymentTypes[];
|
|
5
|
+
supportedEnvironments: SupportedEnvironments[];
|
|
6
|
+
awsRegion: string;
|
|
7
|
+
awsAccountId: string;
|
|
8
|
+
awsKeyPairName?: string;
|
|
9
|
+
awsRoleToAssume?: string;
|
|
10
|
+
awsDeploymentSqsArn?: string;
|
|
11
|
+
ciSubDomain?: string;
|
|
12
|
+
ciInternalServerPort?: string | number;
|
|
13
|
+
ciServerName?: string;
|
|
14
|
+
ciDockerNetworkName?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DeploymentPlatform extends Required<Omit<DeploymentPlatformTemplate, 'awsRoleToAssume' | 'awsKeyPairName'>> {
|
|
18
|
+
activeEnvironment: SupportedEnvironments;
|
|
19
|
+
awsRoleToAssume?: string;
|
|
20
|
+
awsKeyPairName?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export enum SupportedRegions {
|
|
24
|
+
North = 'eu-north-1'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export enum SupportedEnvironments {
|
|
28
|
+
LOCAL = 'local',
|
|
29
|
+
QA = 'qa',
|
|
30
|
+
TEST = 'test',
|
|
31
|
+
DEMO = 'demo',
|
|
32
|
+
PROD = 'prod'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export enum SupportedDeploymentTypes {
|
|
36
|
+
Container = 'CONTAINER',
|
|
37
|
+
// Static = 'STATIC'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DeploymentTemplate {
|
|
41
|
+
type: SupportedDeploymentTypes;
|
|
42
|
+
targetDeploymentPlatform: string;
|
|
43
|
+
subdomain?: string;
|
|
44
|
+
env?: {
|
|
45
|
+
shared?: { [name: string]: string | number };
|
|
46
|
+
prod?: { [name: string]: string | number };
|
|
47
|
+
test?: { [name: string]: string | number };
|
|
48
|
+
qa?: { [name: string]: string | number };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ContainerDeploymentTemplate extends DeploymentTemplate {
|
|
53
|
+
type: SupportedDeploymentTypes.Container;
|
|
54
|
+
dockerFile: string;
|
|
55
|
+
dockerContext: string;
|
|
56
|
+
image: string;
|
|
57
|
+
hostPort: number;
|
|
58
|
+
containerPort: number;
|
|
59
|
+
registry: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ContainerDeploymentRequest extends ContainerDeploymentTemplate {
|
|
63
|
+
authentication?: string;
|
|
64
|
+
username?: string;
|
|
65
|
+
}
|