Skip to content

Commit 00ce73a

Browse files
committed
Initial commit
0 parents  commit 00ce73a

File tree

11 files changed

+389
-0
lines changed

11 files changed

+389
-0
lines changed

.clang-format

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Language: JavaScript
2+
BasedOnStyle: Google
3+
ColumnLimit: 80
4+
IndentWidth: 4

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/node_modules
2+
/dist

.travis.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
os: linux
2+
language: generic
3+
4+
services:
5+
- docker
6+
7+
script:
8+
- ./build.sh
9+
10+
deploy:
11+
- provider: releases
12+
token: $GITHUB_OAUTH_TOKEN
13+
file_glob: true
14+
file: "dist/lambda.zip"
15+
skip_cleanup: true
16+
cleanup: false
17+
on:
18+
tags: true

build.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
if command -v podman;then
5+
DOCKER=podman
6+
elif command -v docker;then
7+
DOCKER=docker
8+
else
9+
echo "Could not locate any docker runtime"
10+
exit 1
11+
fi
12+
13+
IMAGE=local/mageops-node-coordinator-build
14+
15+
"$DOCKER" build -t "$IMAGE" ./build
16+
"$DOCKER" run --rm -v "$PWD:/workdir:z" "$IMAGE"

build/Dockerfile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM node:12-alpine
2+
3+
RUN apk add --no-cache zip
4+
5+
VOLUME /workdir
6+
WORKDIR /workdir
7+
8+
ENTRYPOINT set -ex && yarn install \
9+
&& rm -rf dist \
10+
&& yarn build \
11+
&& rm -rf node_modules \
12+
&& yarn install --prod \
13+
&& mv node_modules dist/node_modules \
14+
&& (cd dist && zip -r9 /tmp/lambda-$$.zip *) \
15+
&& rm dist -rf \
16+
&& mkdir -p dist \
17+
&& mv /tmp/lambda-$$.zip dist/lambda.zip

package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "mageops-node-coordinator",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"private": true,
7+
"scripts": {
8+
"build": "tsc"
9+
},
10+
"devDependencies": {
11+
"@types/node": "^13.13.1",
12+
"typescript": "^3.8.3"
13+
},
14+
"dependencies": {
15+
"aws-sdk": "^2.659.0"
16+
}
17+
}

src/ec2-facade.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {EC2} from 'aws-sdk';
2+
3+
export class EC2Facade {
4+
protected ec2 = new EC2();
5+
6+
7+
describeInstances(req: EC2.DescribeInstancesRequest):
8+
Promise<EC2.DescribeInstancesResult> {
9+
return new Promise(
10+
(resolve, reject) =>
11+
this.ec2.describeInstances(req, (err, data) => {
12+
if (!!err) {
13+
reject(err);
14+
} else {
15+
resolve(data);
16+
}
17+
}));
18+
}
19+
20+
createTags(req: EC2.CreateTagsRequest): Promise<{}> {
21+
return new Promise(
22+
(resolve, reject) => this.ec2.createTags(req, (err, data) => {
23+
if (!!err) {
24+
reject(err);
25+
} else {
26+
resolve(data);
27+
}
28+
}));
29+
}
30+
31+
deleteTags(req: EC2.DeleteTagsRequest): Promise<{}> {
32+
return new Promise(
33+
(resolve, reject) => this.ec2.deleteTags(req, (err, data) => {
34+
if (!!err) {
35+
reject(err);
36+
} else {
37+
resolve(data);
38+
}
39+
}));
40+
}
41+
42+
createInstanceTags(instance: EC2.Instance, tags: EC2.TagList): Promise<{}> {
43+
const id = instance.InstanceId;
44+
if (!id) {
45+
throw Error('InstanceId is missing');
46+
}
47+
return this.createTags({
48+
Resources: [id],
49+
Tags: tags,
50+
});
51+
}
52+
53+
deleteInstanceTags(instance: EC2.Instance, tags: EC2.TagList): Promise<{}> {
54+
const id = instance.InstanceId;
55+
if (!id) {
56+
throw Error('InstanceId is missing');
57+
}
58+
return this.deleteTags({
59+
Resources: [id],
60+
Tags: tags,
61+
});
62+
}
63+
}

src/index.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {EC2} from 'aws-sdk';
2+
import {exactTag, except, flatten, instanceAge, namedTag} from 'utils';
3+
4+
import {EC2Facade} from './ec2-facade';
5+
6+
const cronTagName = 'Cron';
7+
const cronTagValue = 'Present';
8+
9+
class CreateTags {
10+
private project: string;
11+
private environment: string;
12+
private globalFilter: EC2.FilterList = [
13+
{Name: 'tag:Infrastructure', Values: ['mageops']},
14+
];
15+
16+
private projectFilter: EC2.FilterList;
17+
18+
private appNodeFilter: EC2.FilterList = [
19+
{Name: 'tag:Role', Values: ['app']},
20+
{Name: 'instance-state-name', Values: ['running']},
21+
];
22+
23+
private ec2 = new EC2Facade();
24+
25+
constructor() {
26+
const project = process.env['PROJECT'];
27+
if (!project) {
28+
throw Error('PROJECT not specified');
29+
}
30+
this.project = project;
31+
32+
const environment = process.env['ENVIRONMENT'];
33+
if (!environment) {
34+
throw Error('ENVIRONMENT not specified');
35+
}
36+
this.environment = environment;
37+
console.log(
38+
`Using project: ${this.project}, environment: ${this.environment}`);
39+
40+
this.projectFilter = [
41+
{Name: 'tag:Project', Values: [this.project]},
42+
{Name: 'tag:Environment', Values: [this.environment]},
43+
];
44+
}
45+
46+
async tagCron() {
47+
const reservations = await this.ec2.describeInstances({
48+
Filters: [
49+
...this.globalFilter,
50+
...this.projectFilter,
51+
...this.appNodeFilter,
52+
],
53+
});
54+
55+
const instances =
56+
(reservations.Reservations ?? []).map(r => r.Instances)
57+
.filter((i): i is EC2.InstanceList => !!i)
58+
.reduce(flatten, [])
59+
.sort(instanceAge);
60+
61+
const [oldest, ...rest] = instances;
62+
63+
if (!oldest) {
64+
console.log(`There is no instances found!`);
65+
return;
66+
}
67+
68+
const updates = [];
69+
70+
if(!oldest.Tags?.find(exactTag(cronTagName, cronTagValue))) {
71+
updates.push(this.ec2.createInstanceTags(oldest, [
72+
{Key: cronTagName, Value: cronTagValue},
73+
]));
74+
}
75+
76+
for (const instance of rest) {
77+
if(instance.Tags?.find(exactTag(cronTagName, cronTagValue))) {
78+
updates.push(
79+
this.ec2.deleteInstanceTags(
80+
instance, instance.Tags.filter(namedTag(cronTagName))),
81+
);
82+
}
83+
}
84+
85+
await Promise.all(updates);
86+
}
87+
}
88+
89+
export async function handler() {
90+
const tagger = new CreateTags();
91+
await tagger.tagCron();
92+
93+
return {
94+
statusCode: 200,
95+
body: 'OK',
96+
};
97+
}

src/utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {EC2} from 'aws-sdk';
2+
3+
export function flatten<T>(acc: T[], item: T[]): T[] {
4+
return [...acc, ...item];
5+
}
6+
7+
export function instanceAge(a: EC2.Instance, b: EC2.Instance): number {
8+
// ?? is not formatted correctly
9+
// clang-format off
10+
const aTime = a.LaunchTime ?.getTime() ?? Infinity;
11+
const bTime = b.LaunchTime ?.getTime() ?? Infinity;
12+
// clang-format on
13+
return aTime - bTime;
14+
}
15+
16+
export function exactTag(name: string, value: string): (tag: EC2.Tag) =>
17+
boolean {
18+
return (tag) => tag.Key === name && tag.Value === value;
19+
}
20+
21+
export function namedTag(name: string): (tag: EC2.Tag) => boolean {
22+
return (tag) => tag.Key === name;
23+
}
24+
25+
export function except<T>(fn: (i: T) => boolean): (i: T) => boolean {
26+
return (i) => !fn(i);
27+
}

tsconfig.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2019",
4+
"module": "commonjs",
5+
"strict": true,
6+
"noFallthroughCasesInSwitch": true,
7+
"strictBindCallApply": true,
8+
"noImplicitReturns": true,
9+
"experimentalDecorators": true,
10+
"downlevelIteration": true,
11+
"moduleResolution": "node",
12+
"esModuleInterop": true,
13+
"forceConsistentCasingInFileNames": true,
14+
"outDir": "./dist",
15+
"sourceRoot": "./src",
16+
"declaration": true,
17+
"sourceMap": true,
18+
"baseUrl": "./src",
19+
}
20+
}

0 commit comments

Comments
 (0)