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: update the method by which IAM roles are applied #223

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 5 additions & 2 deletions deploy/googleDeploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const monitorDeployment = require('../shared/monitorDeployment');
const uploadArtifacts = require('./lib/uploadArtifacts');
const updateDeployment = require('./lib/updateDeployment');
const cleanupDeploymentBucket = require('./lib/cleanupDeploymentBucket');
const setIamPolicy = require('./lib/setIamPolicy');

class GoogleDeploy {
constructor(serverless, options) {
Expand All @@ -26,7 +27,8 @@ class GoogleDeploy {
monitorDeployment,
uploadArtifacts,
updateDeployment,
cleanupDeploymentBucket
cleanupDeploymentBucket,
setIamPolicy
);

this.hooks = {
Expand All @@ -37,7 +39,8 @@ class GoogleDeploy {
.then(this.createDeployment)
.then(this.setDeploymentBucketName)
.then(this.uploadArtifacts)
.then(this.updateDeployment),
.then(this.updateDeployment)
.then(this.setIamPolicy),

'after:deploy:deploy': () => BbPromise.bind(this).then(this.cleanupDeploymentBucket),
};
Expand Down
4 changes: 4 additions & 0 deletions deploy/googleDeploy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('GoogleDeploy', () => {
let uploadArtifactsStub;
let updateDeploymentStub;
let cleanupDeploymentBucketStub;
let setIamPolicyStub;

beforeEach(() => {
validateStub = sinon.stub(googleDeploy, 'validate').returns(BbPromise.resolve());
Expand All @@ -62,6 +63,7 @@ describe('GoogleDeploy', () => {
cleanupDeploymentBucketStub = sinon
.stub(googleDeploy, 'cleanupDeploymentBucket')
.returns(BbPromise.resolve());
setIamPolicyStub = sinon.stub(googleDeploy, 'setIamPolicy').returns(BbPromise.resolve());
});

afterEach(() => {
Expand All @@ -72,6 +74,7 @@ describe('GoogleDeploy', () => {
googleDeploy.uploadArtifacts.restore();
googleDeploy.updateDeployment.restore();
googleDeploy.cleanupDeploymentBucket.restore();
googleDeploy.setIamPolicy.restore();
});

it('should run "before:deploy:deploy" promise chain', () =>
Expand All @@ -86,6 +89,7 @@ describe('GoogleDeploy', () => {
expect(setDeploymentBucketNameStub.calledAfter(createDeploymentStub)).toEqual(true);
expect(uploadArtifactsStub.calledAfter(createDeploymentStub)).toEqual(true);
expect(updateDeploymentStub.calledAfter(uploadArtifactsStub)).toEqual(true);
expect(setIamPolicyStub.calledAfter(updateDeploymentStub)).toEqual(true);
}));

it('should run "after:deploy:deploy" promise chain', () =>
Expand Down
73 changes: 73 additions & 0 deletions deploy/lib/setIamPolicy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use strict';

const ServerlessError = require('serverless/lib/classes/Error').ServerlessError;

module.exports = {
setIamPolicy() {
return Promise.resolve()
.then(() => this.getFunctions())
.then(() => this.setPolicies());
},

getFunctions() {
const project = this.serverless.service.provider.project;
const region = this.options.region;

const params = {
parent: `projects/${project}/locations/${region}`,
};

return this.provider
.request('cloudfunctions', 'projects', 'locations', 'functions', 'list', params)
.then((response) => {
return response.functions;
});
},

setPolicies(functions) {
const policies = this.serverless.service.provider.functionIamBindings;

// If there are no IAM policies configured with any function, there is nothing to
// do here.
if (!policies || !Object.keys(policies).length) {
return Promise.resolve();
}
this.serverless.cli.log('Setting IAM policies...');

const promises = [];
Object.entries(policies).forEach((entry) => {
const func = functions.find((fn) => {
return fn.name === entry[0];
});
if (func) {
const params = {
resource: func.name,
requestBody: {
policy: {
bindings: entry[1],
},
},
};

promises.push(
this.provider.request(
'cloudfunctions',
'projects',
'locations',
'functions',
'setIamPolicy',
params
)
);
} else {
const errorMessage = [
`Unable to set IAM bindings (${entry[1]}) for "${entry[0]}": function not found for`,
` project "${this.serverless.service.provider.project}" in region "${this.options.region}".`,
].join('');
throw new ServerlessError(errorMessage);
}
});

return Promise.all(promises);
},
};
191 changes: 191 additions & 0 deletions deploy/lib/setIamPolicy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
'use strict';

const sinon = require('sinon');

const GoogleProvider = require('../../provider/googleProvider');
const GoogleDeploy = require('../googleDeploy');
const Serverless = require('../../test/serverless');

describe('SetIamPolicy', () => {
let serverless;
let googleDeploy;
let requestStub;

beforeEach(() => {
serverless = new Serverless();
serverless.service.service = 'my-service';
serverless.service.provider = {
project: 'my-project',
};
serverless.config = {
servicePath: 'tmp',
};
serverless.setProvider('google', new GoogleProvider(serverless));
const options = {
stage: 'dev',
region: 'us-central1',
};
googleDeploy = new GoogleDeploy(serverless, options);
requestStub = sinon.stub(googleDeploy.provider, 'request');
});

afterEach(() => {
googleDeploy.provider.request.restore();
});

describe('#setIamPolicy', () => {
let getFunctionsStub;
let setPoliciesStub;

beforeEach(() => {
getFunctionsStub = sinon.stub(googleDeploy, 'getFunctions').returns(Promise.resolve());
setPoliciesStub = sinon.stub(googleDeploy, 'setPolicies').returns(Promise.resolve());
});

afterEach(() => {
googleDeploy.getFunctions.restore();
googleDeploy.setPolicies.restore();
});

it('should run the promise chain', () => {
googleDeploy.setIamPolicy().then(() => {
expect(getFunctionsStub.calledOnce).toEqual(true);
expect(setPoliciesStub.calledAfter(getFunctionsStub));
});
});
});

describe('#getFunctions', () => {
it('should return "undefined" if no functions are found', () => {
requestStub.returns(Promise.resolve([]));

return googleDeploy.getFunctions().then((foundFunctions) => {
expect(foundFunctions).toEqual(undefined);
expect(
requestStub.calledWithExactly(
'cloudfunctions',
'projects',
'locations',
'functions',
'list',
{
parent: 'projects/my-project/locations/us-central1',
}
)
).toEqual(true);
});
});

it('should return all functions that are found', () => {
const response = {
functions: [{ name: 'cloud-function-1' }, { name: 'cloud-function-2' }],
};
requestStub.returns(Promise.resolve(response));

return googleDeploy.getFunctions().then((foundFunctions) => {
expect(foundFunctions).toEqual([
{ name: 'cloud-function-1' },
{ name: 'cloud-function-2' },
]);
expect(
requestStub.calledWithExactly(
'cloudfunctions',
'projects',
'locations',
'functions',
'list',
{
parent: 'projects/my-project/locations/us-central1',
}
)
).toEqual(true);
});
});
});

describe('#setPolicies', () => {
let consoleLogStub;

beforeEach(() => {
consoleLogStub = sinon.stub(googleDeploy.serverless.cli, 'log').returns();
googleDeploy.serverless.service.provider.functionIamBindings = {};
});

afterEach(() => {
googleDeploy.serverless.cli.log.restore();
});

it('should resolve if functionIamBindings is undefined', () => {
const foundFunctions = [{ name: 'cloud-function-1' }, { name: 'cloud-function-2' }];
delete googleDeploy.serverless.service.provider.functionIamBindings;

return googleDeploy.setPolicies(foundFunctions).then(() => {
expect(consoleLogStub.calledOnce).toEqual(false);
});
});

it('should resolve if there are no IAM policies configured', () => {
const foundFunctions = [{ name: 'cloud-function-1' }, { name: 'cloud-function-2' }];

return googleDeploy.setPolicies(foundFunctions).then(() => {
expect(consoleLogStub.calledOnce).toEqual(false);
});
});

it('should error if there are no existing functions to apply configured IAM to', () => {
const foundFunctions = [];
googleDeploy.serverless.service.provider.functionIamBindings = {
'cloud-function-1': [{ role: 'roles/cloudfunctions.invoker', members: ['allUsers'] }],
};

expect(() => googleDeploy.setPolicies(foundFunctions)).toThrow(Error);
expect(consoleLogStub.calledOnce).toEqual(true);
expect(requestStub.calledOnce).toEqual(false);
});

it('should error if a configured function is not found', () => {
const foundFunctions = [{ name: 'cloud-function-2' }];
googleDeploy.serverless.service.provider.functionIamBindings = {
'cloud-function-1': [{ role: 'roles/cloudfunctions.invoker', members: ['allUsers'] }],
};

expect(() => googleDeploy.setPolicies(foundFunctions)).toThrow(Error);
expect(consoleLogStub.calledOnce).toEqual(true);
expect(requestStub.calledOnce).toEqual(false);
});

it('should set the IAM policy for the configured functions', () => {
const foundFunctions = [{ name: 'cloud-function-1' }, { name: 'cloud-function-2' }];
googleDeploy.serverless.service.provider.functionIamBindings = {
'cloud-function-2': [{ role: 'roles/cloudfunctions.invoker', members: ['allUsers'] }],
};
requestStub.returns(Promise.resolve());

return googleDeploy.setPolicies(foundFunctions).then(() => {
expect(consoleLogStub.calledOnce).toEqual(true);
expect(
requestStub.calledWithExactly(
'cloudfunctions',
'projects',
'locations',
'functions',
'setIamPolicy',
{
resource: 'cloud-function-2',
requestBody: {
policy: {
bindings: [
{
role: 'roles/cloudfunctions.invoker',
members: ['allUsers'],
},
],
},
},
}
)
).toEqual(true);
});
});
});
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"chalk": "^3.0.0",
"fs-extra": "^8.1.0",
"googleapis": "^50.0.0",
"lodash": "^4.17.21"
"lodash": "^4.17.21",
"serverless": "^2.41.1"
},
"devDependencies": {
"@commitlint/cli": "^9.1.2",
Expand Down
Loading