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

fix: Endpoint to invalidate cache #67

Merged
merged 3 commits into from
Nov 21, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/custom-environment-variables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ strategy:

ecosystem:
ui: ECOSYSTEM_UI
api: ECOSYSTEM_API
1 change: 1 addition & 0 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,4 @@ strategy:

ecosystem:
ui: https://cd.screwdriver.cd
api: https://api.screwdriver.cd
35 changes: 35 additions & 0 deletions helpers/aws.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,41 @@ class AwsClient {
});
});
}

/**
* Delete all cached objects at cachePath
* @method invalidateCache
* @param {String} cachePath Path to cache
* @param {Function} callback callback function
*/
invalidateCache(cachePath, callback) {
const self = this;

let params = {
Bucket: this.bucket,
Prefix: `caches/${cachePath}`
};

return this.client.listObjects(params, (e, data) => {
if (e) return callback(e);

if (data.isTruncated) return callback();
d2lam marked this conversation as resolved.
Show resolved Hide resolved

params = { Bucket: this.bucket };
params.Delete = { Objects: [] };

data.Contents.forEach((content) => {
params.Delete.Objects.push({ Key: content.Key });
});

return this.client.deleteObjects(params, (err, res) => {
if (err) return callback(err);
if (res.isTruncated) return self.invalidateCache(this.bucket, callback);
d2lam marked this conversation as resolved.
Show resolved Hide resolved

return callback();
});
});
}
}

module.exports = AwsClient;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"hoek": "^5.0.3",
"inert": "^5.1.0",
"joi": "13.1.2",
"request": "^2.88.0",
"screwdriver-data-schema": "^18.11.5",
"vision": "^5.3.0",
"winston": "^2.2.0"
Expand Down
91 changes: 91 additions & 0 deletions plugins/caches.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const joi = require('joi');
const boom = require('boom');
const config = require('config');
const AwsClient = require('../helpers/aws');
const req = require('request');

const SCHEMA_SCOPE_NAME = joi.string().valid(['events', 'jobs', 'pipelines']).label('Scope Name');
const SCHEMA_SCOPE_ID = joi.number().integer().positive().label('Event/Job/Pipeline ID');
Expand Down Expand Up @@ -332,6 +333,96 @@ exports.plugin = {
}
}
}
}, {
method: 'DELETE',
path: '/caches/{scope}/{id}',
handler: async (request, h) => {
if (strategyConfig.plugin !== 's3') {
return h.response();
}

let cachePath;
const apiUrl = config.get('ecosystem.api');
const payload = {
d2lam marked this conversation as resolved.
Show resolved Hide resolved
url: `${apiUrl}/v4/isAdmin`,
method: 'GET',
headers: {
Authorization: `Bearer ${request.auth.token}`,
'Content-Type': 'application/json'
},
json: true
};

switch (request.params.scope) {
case 'events': {
d2lam marked this conversation as resolved.
Show resolved Hide resolved
return h.response();
}
case 'jobs': {
const jobIdParam = request.params.id;

payload.qs = {
d2lam marked this conversation as resolved.
Show resolved Hide resolved
jobId: jobIdParam
};

cachePath = `jobs/${jobIdParam}/`;
break;
}
case 'pipelines': {
const pipelineIdParam = request.params.id;

payload.qs = {
pipelineId: pipelineIdParam
};

cachePath = `pipelines/${pipelineIdParam}`;
d2lam marked this conversation as resolved.
Show resolved Hide resolved
break;
}
default:
return boom.forbidden('Invalid scope');
}

try {
await req(payload, (err, response) => {
if (!err && response.statusCode === 200) {
d2lam marked this conversation as resolved.
Show resolved Hide resolved
return awsClient.invalidateCache(cachePath, (e) => {
if (e) {
console.log('Failed to invalidate cache: ', e);
}

return Promise.resolve();
});
} else if (!err) {
return Promise.reject(new Error('User cannot invalidate cache.'));
}

return Promise.reject(err);
});
} catch (err) {
return boom.forbidden(err);
}

return h.response();
},
options: {
description: 'Invalidate cache folder',
notes: 'Delete entire cache folder for a job or pipeline',
tags: ['api', 'events', 'jobs', 'pipelines'],
auth: {
strategies: ['token'],
scope: ['user']
d2lam marked this conversation as resolved.
Show resolved Hide resolved
},
plugins: {
'hapi-swagger': {
security: [{ token: [] }]
}
},
validate: {
params: {
scope: SCHEMA_SCOPE_NAME,
id: SCHEMA_SCOPE_ID
}
}
}
}]);
}
};
22 changes: 22 additions & 0 deletions test/helpers/aws.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,26 @@ describe('aws helper test', () => {
done();
});
});

it('returns err if fails to listObjects', (done) => {
const err = new Error('failed to run listObjects');

clientMock.prototype.listObjects = sinon.stub().yieldsAsync(err);

return awsClient.invalidateCache(cacheKey, (e) => {
assert.deepEqual(e, err);
done();
});
});

it('returns err if fails to deleteObjects', (done) => {
const err = new Error('failed to run deleteObjects');

clientMock.prototype.listObjects = sinon.stub().yieldsAsync(err);

return awsClient.invalidateCache(cacheKey, (e) => {
assert.deepEqual(e, err);
done();
});
});
});
69 changes: 68 additions & 1 deletion test/plugins/caches.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('events plugin test', () => {
let plugin;
let server;
let awsClientMock;
let reqMock;
let configMock;

before(() => {
Expand All @@ -31,11 +32,17 @@ describe('events plugin test', () => {
};

awsClientMock = sinon.stub().returns({
updateLastModified: sinon.stub().yields(null)
updateLastModified: sinon.stub().yields(null),
invalidateCache: sinon.stub().yields(null)
});

reqMock = sinon.stub().yields(null, {
statusCode: 403
});

mockery.registerMock('../helpers/aws', awsClientMock);
mockery.registerMock('config', configMock);
mockery.registerMock('request', reqMock);

// eslint-disable-next-line global-require
plugin = require('../../plugins/caches');
Expand Down Expand Up @@ -629,4 +636,64 @@ describe('events plugin test', () => {
});
}));
});

describe('DELETE /caches/:scope/:id', () => {
let getOptions;
let putOptions;
let deleteOptions;

beforeEach(() => {
getOptions = {
headers: {
'x-foo': 'bar'
},
credentials: {
jobId: mockJobID,
scope: ['build']
},
url: `/caches/jobs/${mockJobID}/foo`
};
putOptions = {
method: 'PUT',
payload: 'THIS IS A TEST',
headers: {
'x-foo': 'bar',
'content-type': 'text/plain',
ignore: 'true'
},
credentials: {
jobId: mockJobID,
scope: ['build']
},
url: `/caches/jobs/${mockJobID}/foo`
};
deleteOptions = {
method: 'DELETE',
headers: {
'x-foo': 'bar',
'content-type': 'text/plain',
ignore: 'true'
},
credentials: {
username: 'testuser',
scope: ['user']
},
url: `/caches/jobs/${mockJobID}`
};
});

it('Throws error if user cannot invalidate cache', () =>
server.inject(putOptions).then((postResponse) => {
assert.equal(postResponse.statusCode, 202);

return server.inject(getOptions).then((getResponse) => {
assert.equal(getResponse.statusCode, 200);

return server.inject(deleteOptions).then((deleteResponse) => {
assert.equal(deleteResponse.statusCode, 403);
});
});
})
);
});
});