Skip to content

Commit

Permalink
adds intial consul support and unit test stub
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronkvanmeerten committed Dec 6, 2024
1 parent 2a03961 commit 06c99b9
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 0 deletions.
54 changes: 54 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@types/shortid": "0.0.31",
"@types/sshpk": "^1.17.3",
"bee-queue": "^1.6.0",
"consul": "^2.0.1",
"dotenv": "^16.3.1",
"dots-wrapper": "^3.11.3",
"envalid": "^8.0.0",
Expand Down
102 changes: 102 additions & 0 deletions src/consul.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Consul from 'consul';
import { Context } from './context';
import { GetItem } from 'consul/lib/kv';
import InstanceStore, { InstanceGroup } from './instance_store';

Check failure on line 4 in src/consul.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'InstanceStore' is defined but never used

// implments the InstanceStore interface using consul K/V API calls
// uses the got library to make HTTP requests

export interface ConsulOptions {
host: string;
port: number;
secure: boolean;
groupsPrefix?: string;
}

export default class ConsulStore {
private client: Consul;
private groupsPrefix = 'autoscaler/groups/';

constructor(options: ConsulOptions) {
this.client = new Consul(options);
if (options.groupsPrefix) {
this.groupsPrefix = options.groupsPrefix;
}
}

async getInstanceGroup(ctx: Context, group: string): Promise<InstanceGroup> {
try {
const { Value } = await this.fetch(ctx, `${this.groupsPrefix}${group}`);
return <InstanceGroup>JSON.parse(Value);
} catch (err) {
ctx.logger.error(`Failed to get instance group from consul: ${err}`, { err });
throw err;
}
}

async getAllInstanceGroups(ctx: Context): Promise<InstanceGroup[]> {
try {
const keys = await this.fetchInstanceGroups(ctx);
const groups = await Promise.all(keys.map((key) => this.getInstanceGroup(ctx, key)));
return groups;
} catch (err) {
ctx.logger.error(`Failed to get all instance groups from consul: ${err}`, { err });
throw err;
}
}

async fetchInstanceGroups(ctx: Context): Promise<string[]> {
ctx.logger.debug('fetching consul k/v keys');
const res = await this.client.kv.get({ key: this.groupsPrefix, recurse: true });
ctx.logger.debug('received consul k/v keys', { res });
if (!res) {
return [];
}
return Object.entries(res).map(([_k, v]) => v.Key.replace(this.groupsPrefix, ''));
}

async upsertInstanceGroup(ctx: Context, group: InstanceGroup): Promise<boolean> {
try {
await this.write(ctx, `${this.groupsPrefix}${group.name}`, JSON.stringify(group));
return true;
} catch (err) {
ctx.logger.error(`Failed to upsert instance group into consul: ${err}`, { group: group.name, err });
return false;
}
}

async deleteInstanceGroup(ctx: Context, group: string): Promise<boolean> {
try {
await this.delete(`${this.groupsPrefix}${group}`);
return true;
} catch (err) {
ctx.logger.error(`Failed to delete instance group from consul: ${err}`, { group, err });
return false;
}
}

async fetch(ctx: Context, key: string): Promise<GetItem | undefined> {
ctx.logger.debug(`reading consul k/v key`, { key });
const v = await this.client.kv.get(key);
ctx.logger.debug(`received consul k/v item`, { v });
return v;
}

async write(ctx: Context, key: string, value: string): Promise<boolean> {
try {
const res = await this.client.kv.set(key, value);
if (!res) {
ctx.logger.error(`Failed to write to consul`);
}
return res;
} catch (err) {
ctx.logger.error(`Failed to write to consul: ${err}`, { err });
return false;
}
}

async delete(key: string): Promise<boolean> {
await this.client.kv.del(key);
return true;
}
}
65 changes: 65 additions & 0 deletions src/test/consul.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import AutoscalerLogger from '../logger';
import assert from 'node:assert';
import test, { afterEach, describe, mock } from 'node:test';

import ConsulClient, { ConsulOptions } from '../consul';

const asLogger = new AutoscalerLogger({ logLevel: 'debug' });
const logger = asLogger.createLogger('debug');

const ctx = { logger };
ctx.logger.debug = mock.fn();
ctx.logger.error = mock.fn();

const options = <ConsulOptions>{
host: 'localhost',
port: 8500,
secure: false,
groupsPrefix: '_test/autoscaler/groups/',
};
const client = new ConsulClient(options);

const group = {
name: 'test',
type: 'test',
region: 'test',
environment: 'test',
enableScheduler: true,
tags: {
test: 'test',
},
};

describe('ConsulClient', () => {
afterEach(() => {
mock.restoreAll();
});

describe('testListInstanceGroups', () => {
test('will list all instance groups', async () => {
const res = await client.fetchInstanceGroups(ctx);
assert.strictEqual(res.length, 0);
});

test('will upsert a test group', async () => {
const res = await client.upsertInstanceGroup(ctx, group);
assert.strictEqual(res, true);
});

test('will find upserted group when listing all instance groups', async () => {
const res = await client.fetchInstanceGroups(ctx);
assert.strictEqual(res.length, 1);
assert.strictEqual(res[0], group.name);

const res2 = await client.getInstanceGroup(ctx, group.name);
assert.deepEqual(res2, group);
});

test('will delete upserted test group', async () => {
const res = await client.deleteInstanceGroup(ctx, group.name);
assert.strictEqual(res, true);
});
});
});

0 comments on commit 06c99b9

Please sign in to comment.