Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FTDCS-196 - Fastly key expiration check merge #189

Merged
merged 10 commits into from
Jun 20, 2022
109 changes: 109 additions & 0 deletions src/checks/fastlyKeyExpiration.check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const fetch = require('node-fetch');
const moment = require('moment');
const logger = require('@financial-times/n-logger').default;
const Check = require('./check');
const status = require('./status');

const fastlyApiEndpoint = 'https://api.fastly.com/tokens/self';
/**
* @description Polls the current state of a Fastly key expiration date
* alert if the key expires in the next week or two
* alert if the key has a null expiry date
*/
class FastlyKeyExpirationCheck extends Check {
constructor(options) {
super(options);
this.fastlyKey = options.fastlyKey;
this.states = Object.freeze({
PENDING: {
status: status.PENDING,
checkOutput: 'Fastly key check has not yet run',
severity: this.severity
JSRedondo marked this conversation as resolved.
Show resolved Hide resolved
},
FAILED_VALIDATION: {
status: status.FAILED,
checkOutput: 'Fastly key expiration date is due within 2 weeks',
severity: this.severity
},
FAILED_URGENT_VALIDATION: {
status: status.FAILED,
checkOutput: 'Fastly key is expired',
severity: 1
},
FAILED_DATE: {
status: status.FAILED,
checkOutput: 'Invalid Fastly key expiring date',
severity: this.severity
},
ERRORED: {
status: status.ERRORED,
checkOutput: 'Fastly key check failed to fetch data',
severity: this.severity
},
PASSED: {
status: status.PASSED,
checkOutput: 'Fastly key expiration date is ok',
severity: this.severity
}
});
this.setState(this.states.PENDING);
}

setState(state) {
// To be able to assign a varialble number of parameters
Object.assign(this, state);
}

async getFastlyKeyMetadata() {
try {
const result = await fetch(fastlyApiEndpoint, {
headers: { 'Fastly-Key': this.fastlyKey }
});
const json = await result.json();
return json;
} catch (error) {
logger.error('Failed to get Fastly key metadata', error.message);
this.setState(this.states.ERRORED);
throw error;
}
}

parseStringDate(stringDate) {
const date = moment(stringDate, 'YYYY-MM-DDTHH:mm:ssZ');
if (!date.isValid()) {
logger.warn(`Invalid Fastly Key expiration date ${stringDate}`);
this.setState(this.states.FAILED_DATE);
throw new Error('Invalid date');
}
return date;
}

async getExpirationDate() {
const metadata = await this.getFastlyKeyMetadata();
const expirationDate = this.parseStringDate(metadata['expires_at']);
return expirationDate;
}

checkExpirationDate(expirationDate) {
const now = moment();
const limitDate = moment().add(2, 'weeks');
switch(true){
case expirationDate.isAfter(limitDate): return this.states.PASSED;
case expirationDate.isBefore(now): return this.states.FAILED_URGENT_VALIDATION;
default: return this.states.FAILED_VALIDATION
}
}

async tick() {
try {
const expirationDate = await this.getExpirationDate();
const state = this.checkExpirationDate(expirationDate);
this.setState(state);
} catch {
// This catch is meant to end the execution of the tick function.
// An expecific state has been set for each error type.
}
}
}

module.exports = FastlyKeyExpirationCheck;
3 changes: 2 additions & 1 deletion src/checks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ module.exports = {
graphiteThreshold: require('./graphiteThreshold.check'),
graphiteWorking: require('./graphiteWorking.check'),
cloudWatchAlarm: require('./cloudWatchAlarm.check'),
cloudWatchThreshold: require('./cloudWatchThreshold.check')
cloudWatchThreshold: require('./cloudWatchThreshold.check'),
fastlyKeys: require('./fastlyKeyExpiration.check')
};
185 changes: 185 additions & 0 deletions test/fastlyKeyExpiration.check.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
const sinon = require('sinon');
const { expect } = require('chai');
const proxyquire = require('proxyquire').noCallThru().noPreserveCache();
const logger = require('@financial-times/n-logger').default;
const FastlyCheck = require('../src/checks/fastlyKeyExpiration.check');
const status = require('../src/checks/status');

const millisecondsFortnight = 14 * 24 * 60 * 60 * 1000;
const defaultOptions = {
name: 'fastlyKeyExpiration',
severity: 2,
businessImpact: 'b',
panicGuide: 'p',
technicalSummary: 't',
fastlyKey: 'k'
};

describe('Fastly Key Expiration Check', () => {
beforeEach(() => {
sinon.spy(logger);
});
afterEach(() => {
sinon.restore();
});
it('FastlyKeyExpirationCheck instance set & freeze states properly ', () => {
const fastlyKeyExpirationCheck = new FastlyCheck(defaultOptions);
const initialStates = {
PENDING: {
status: status.PENDING,
checkOutput: 'Fastly key check has not yet run',
severity: 2
},
FAILED_VALIDATION: {
status: status.FAILED,
checkOutput: 'Fastly key expiration date is due within 2 weeks',
severity: 2
},
FAILED_URGENT_VALIDATION: {
status: status.FAILED,
checkOutput: 'Fastly key is expired',
severity: 1
},
FAILED_DATE: {
status: status.FAILED,
checkOutput: 'Invalid Fastly key expiring date',
severity: 2
},
ERRORED: {
status: status.ERRORED,
checkOutput: 'Fastly key check failed to fetch data',
severity: 2
},
PASSED: {
status: status.PASSED,
checkOutput: 'Fastly key expiration date is ok',
severity: 2
}
};
expect(fastlyKeyExpirationCheck.states).to.be.deep.equal(initialStates);
fastlyKeyExpirationCheck.severity = 1;
expect(fastlyKeyExpirationCheck.states).to.be.deep.equal(initialStates);
});

it('returns ok=false if fastly key check has not yet run', async () => {
sinon.stub(FastlyCheck.prototype, 'tick');
const fastlyCheck = new FastlyCheck(defaultOptions);
const result = fastlyCheck.getStatus();
expect(result.ok).to.be.false;
// Default error message for ERRORED states in parent function Check
expect(result.checkOutput).to.be.equal(
fastlyCheck.states.PENDING.checkOutput
);
expect(result.severity).to.be.equal(fastlyCheck.states.PENDING.severity);
});
it('returns ok=false if key check failed to fetch data & logs errors', async () => {
// Arrange
const FastlyCheck = proxyquire('../src/checks/fastlyKeyExpiration.check', {
'node-fetch': sinon.stub().rejects(new Error('Timeout'))
});
const fastlyCheck = new FastlyCheck(defaultOptions);
// Force the update of the state
await fastlyCheck.tick();
// Act
const result = fastlyCheck.getStatus();
// Assert
expect(result.ok).to.be.false;
// Default error message for ERRORED states in parent function Check
expect(result.checkOutput).to.be.equal('Healthcheck failed to execute');
expect(result.severity).to.be.equal(fastlyCheck.states.ERRORED.severity);
expect(logger.error.args).to.be.deep.equal([
['Failed to get Fastly key metadata', 'Timeout']
]);
});
it('returns ok=false, severity=1 if expiration date is due now', async () => {
// Arrange
const now = new Date();
const limitDate = new Date(
now.getTime() // now
);
// Fix now as reference time
sinon.useFakeTimers(now);
const fastlyCheck = new FastlyCheck(defaultOptions);
const metadata = { expires_at: limitDate.toISOString() };
sinon.stub(fastlyCheck, 'getFastlyKeyMetadata').resolves(metadata);
// Force the update of the state
await fastlyCheck.tick();
// Act
const result = fastlyCheck.getStatus();
// Assert
expect(result.ok).to.be.false;
expect(result.checkOutput).to.be.equal(
fastlyCheck.states.FAILED_URGENT_VALIDATION.checkOutput
);
expect(result.severity).to.be.equal(
fastlyCheck.states.FAILED_URGENT_VALIDATION.severity
);
});
it('returns ok=false if expiration date is due within 2 weeks', async () => {
// Arrange
const now = new Date();
const limitDate = new Date(
now.getTime() + millisecondsFortnight // 2 weeks from now
);
// Fix now as reference time
sinon.useFakeTimers(now);
const fastlyCheck = new FastlyCheck(defaultOptions);
const metadata = { expires_at: limitDate.toISOString() };
sinon.stub(fastlyCheck, 'getFastlyKeyMetadata').resolves(metadata);
// Force the update of the state
await fastlyCheck.tick();
// Act
const result = fastlyCheck.getStatus();
// Assert
expect(result.ok).to.be.false;
expect(result.checkOutput).to.be.equal(
fastlyCheck.states.FAILED_VALIDATION.checkOutput
);
expect(result.severity).to.be.equal(
fastlyCheck.states.FAILED_VALIDATION.severity
);
});
it('returns ok=true if expiration date is due after 2 weeks', async () => {
// Arrange
const now = new Date();
const limitDate = new Date(
now.getTime() + millisecondsFortnight + 1000 // 2 weeks from now + 1 second
);
// Fix now as reference time
sinon.useFakeTimers(now);
const fastlyCheck = new FastlyCheck(defaultOptions);
const metadata = { expires_at: limitDate.toISOString() };
sinon.stub(fastlyCheck, 'getFastlyKeyMetadata').resolves(metadata);
// Force the update of the state
await fastlyCheck.tick();
// Act
const result = fastlyCheck.getStatus();
// Assert
expect(result.ok).to.be.true;
expect(result.checkOutput).to.be.equal(
fastlyCheck.states.PASSED.checkOutput
);
expect(result.severity).to.be.equal(fastlyCheck.states.PASSED.severity);
});
it('returns ok=false if expiration date is not valid & logs warning', async () => {
// Arrange
const fastlyCheck = new FastlyCheck(defaultOptions);
const metadata = { expires_at: 'aaaa' };
sinon.stub(fastlyCheck, 'getFastlyKeyMetadata').resolves(metadata);
// Force the update of the state
await fastlyCheck.tick();
// Act
const result = fastlyCheck.getStatus();
// Assert
expect(result.ok).to.be.false;
expect(result.checkOutput).to.be.equal(
fastlyCheck.states.FAILED_DATE.checkOutput
);
expect(result.severity).to.be.equal(
fastlyCheck.states.FAILED_DATE.severity
);
expect(logger.warn.args).to.be.deep.equal([
['Invalid Fastly Key expiration date aaaa']
]);
});
});