Skip to content
This repository has been archived by the owner on Dec 9, 2024. It is now read-only.

Commit

Permalink
Add new service binding command to manifest.
Browse files Browse the repository at this point in the history
  • Loading branch information
jthomas committed Jun 27, 2018
1 parent 13ac5e4 commit 7ffcbef
Show file tree
Hide file tree
Showing 9 changed files with 458 additions and 1 deletion.
66 changes: 66 additions & 0 deletions compile/servicebindings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Compile Packages

This plugins compiles the packages in `serverless.yaml` to corresponding [OpenWhisk Packages](https://github.com/openwhisk/openwhisk/blob/master/docs/packages.md)
definitions.

## How it works

`Compile Packages` hooks into the [`package:compileEvents`](/lib/plugins/deploy) lifecycle.

It loops over all packages which are defined in `serverless.yaml`.

### Implicit Packages

Actions can be assigned to packages by setting the function `name` with a package reference.

```yaml
functions:
foo:
handler: handler.foo
name: "myPackage/foo"
bar:
handler: handler.bar
name: "myPackage/bar"
```
In this example, two new actions (`foo` & `bar`) will be created using the `myPackage` package.

Packages which do not exist will be automatically created during deployments. When using the `remove` command, any packages referenced in the `serverless.yml` will be deleted.

### Explicit Packages

Packages can also be defined explicitly to set shared configuration parameters. Default package parameters are merged into event parameters for each invocation.

```yaml
functions:
foo:
handler: handler.foo
name: "myPackage/foo"
resources:
packages:
myPackage:
parameters:
hello: world
```

### Binding Packages

OpenWhisk also supports "binding" external packages into your workspace. Bound packages can have default parameters set for shared actions.

For example, binding the `/whisk.system/cloudant` package into a new package allows you to set default values for the `username`, `password` and `dbname` properties. Actions from this package can then be invoked with having to pass these parameters in.

Define packages explicitly with a `binding` parameter to use this behaviour.

```yaml
resources:
packages:
mySamples:
binding: /whisk.system/cloudant
parameters:
username: bernie
password: sanders
dbname: vermont
```

For more details on package binding, please see the documentation [here](https://github.com/apache/incubator-openwhisk/blob/master/docs/packages.md#creating-and-using-package-bindings).
66 changes: 66 additions & 0 deletions compile/servicebindings/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';

const BbPromise = require('bluebird');

class OpenWhiskCompileServiceBindings {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.provider = this.serverless.getProvider('openwhisk');

this.hooks = {
'package:compileEvents': this.compileServiceBindings.bind(this)
};
}

calculateFunctionName(name, props) {
return props.name || `${this.serverless.service.service}_${name}`;
}

parseServiceBindings(action, properties) {
const name = { action: this.calculateFunctionName(action, properties) }
const bindings = properties.bind || []
const servicebindings = bindings.filter(b => b.service)
.map(b => Object.assign(b.service, name))

const serviceNames = new Set()

for (let sb of servicebindings) {
if (!sb.hasOwnProperty('name')) {
throw new Error(`service binding missing name parameter: ${JSON.stringify(sb)}`)
}

if (serviceNames.has(sb.name)) {
throw new Error(`multiple bindings for same service not supported: ${sb.name}`)
}

serviceNames.add(sb.name)
}

return servicebindings
}

compileFnServiceBindings() {
return this.serverless.service.getAllFunctions()
.map(name => this.parseServiceBindings(name, this.serverless.service.getFunction(name)))
}

compilePkgServiceBindings() {
const manifestResources = this.serverless.service.resources || {}
const packages = manifestResources.packages || {}

return Object.keys(packages).map(name => this.parseServiceBindings(name, packages[name]))
}

compileServiceBindings() {
this.serverless.cli.log('Compiling Service Bindings...');

const fnServiceBindings = this.compileFnServiceBindings()
const pkgServiceBindings = this.compilePkgServiceBindings()

this.serverless.service.bindings = [].concat(...pkgServiceBindings, ...fnServiceBindings)
return BbPromise.resolve();
}
}

module.exports = OpenWhiskCompileServiceBindings;
167 changes: 167 additions & 0 deletions compile/servicebindings/tests/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
'use strict';

const expect = require('chai').expect;
const chaiAsPromised = require('chai-as-promised');

require('chai').use(chaiAsPromised);

const sinon = require('sinon');
const OpenWhiskCompileServiceBindings = require('../index');

describe('OpenWhiskCompileServiceBindings', () => {
let serverless;
let sandbox;
let openwhiskCompileServiceBindings;

beforeEach(() => {
sandbox = sinon.sandbox.create();
serverless = {classes: {Error}, service: {provider: {}, resources: {}, getAllFunctions: () => []}, getProvider: sandbox.spy()};
const options = {
stage: 'dev',
region: 'us-east-1',
};
openwhiskCompileServiceBindings = new OpenWhiskCompileServiceBindings(serverless, options);
serverless.service.service = 'serviceName';
serverless.service.provider = {
namespace: 'testing',
apihost: '',
auth: '',
};

serverless.cli = { consoleLog: () => {}, log: () => {} };
});

afterEach(() => {
sandbox.restore();
});

describe('#parseServiceBindings()', () => {
it('should return empty array when missing service bindings', () => {
const action = 'fnName'
expect(openwhiskCompileServiceBindings.parseServiceBindings(action, {})).to.deep.equal([])
expect(openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: []})).to.deep.equal([])
expect(openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: [{}]})).to.deep.equal([])
expect(openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: [{blah: {}}]})).to.deep.equal([])
})

it('should return array with single service binding property', () => {
const action = 'fnName'
const service = { name: 'my-service', instance: 'my-instance', key: 'mykey' }
const response = { action: `serviceName_fnName`, name: 'my-service', instance: 'my-instance', key: 'mykey' }
const result = openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: [{ service }]})
expect(result).to.deep.equal([response])
})

it('should return array with multiple service binding properties', () => {
const action = 'fnName'
const service_a = { action: `serviceName_fnName`, name: 'my-service-a', instance: 'my-instance-a', key: 'mykey' }
const service_b = { action: `serviceName_fnName`, name: 'my-service-b', instance: 'my-instance-b', key: 'mykey' }
const service_c = { action: `serviceName_fnName`, name: 'my-service-c', instance: 'my-instance-c', key: 'mykey' }
const services = [{ service: service_a }, { service: service_b }, { service: service_c } ]
const result = openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: services})
expect(result).to.deep.equal([service_a, service_b, service_c])
})

it('should throw an error if service binding is missing name', () => {
const service = { instance: 'my-instance-a', key: 'mykey' }
const action = 'fnName'
const services = [{ service }]
expect(() => openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: services}))
.to.throw(Error, /service binding missing name parameter/);
});

it('should throw an error if multiple bindings for same service name', () => {
const action = 'fnName'
const service = { name: 'my-service', instance: 'my-instance-a', key: 'mykey' }
const services = [{ service }, { service }]
expect(() => openwhiskCompileServiceBindings.parseServiceBindings(action, {bind: services}))
.to.throw(Error, /multiple bindings for same service not supported/);
});
})

describe('#compileServiceBindings()', () => {
it('should return service bindings for simple functions', () => {
const fns = {
a: { bind: [{ service: { name: 'service-name-a' } }] },
b: { bind: [{ service: { name: 'service-name-b', instance: 'instance-name' } }] },
c: { bind: [{ service: { name: 'service-name-a' } }, { service: { name: 'service-name-b' } }] },
d: { },
}

const service = openwhiskCompileServiceBindings.serverless.service
service.getAllFunctions = () => Object.keys(fns)
service.getFunction = name => fns[name]

const services = [
{ action: 'serviceName_a', name: 'service-name-a' },
{ action: 'serviceName_b', name: 'service-name-b', instance: 'instance-name' },
{ action: 'serviceName_c', name: 'service-name-a' },
{ action: 'serviceName_c', name: 'service-name-b' }
]
return openwhiskCompileServiceBindings.compileServiceBindings().then(result => {
expect(service.bindings).to.deep.equal(services)
})
})

it('should return service bindings for functions with explicit name', () => {
const fns = {
a: { name: 'some_name', bind: [{ service: { name: 'service-name-a' } }] }
}

const service = openwhiskCompileServiceBindings.serverless.service
service.getAllFunctions = () => Object.keys(fns)
service.getFunction = name => fns[name]

const services = [ { action: 'some_name', name: 'service-name-a' } ]
return openwhiskCompileServiceBindings.compileServiceBindings().then(result => {
expect(service.bindings).to.deep.equal(services)
})
})


it('should return service bindings for packages', () => {
const service = openwhiskCompileServiceBindings.serverless.service
service.resources.packages = {
a: { bind: [{ service: { name: 'service-name-a' } }] },
b: { bind: [{ service: { name: 'service-name-b', instance: 'instance-name' } }] },
c: { bind: [{ service: { name: 'service-name-a' } }, { service: { name: 'service-name-b' } }] },
d: { },
}

const services = [
{ action: 'serviceName_a', name: 'service-name-a' },
{ action: 'serviceName_b', name: 'service-name-b', instance: 'instance-name' },
{ action: 'serviceName_c', name: 'service-name-a' },
{ action: 'serviceName_c', name: 'service-name-b' }
]

return openwhiskCompileServiceBindings.compileServiceBindings().then(() => {
expect(service.bindings).to.deep.equal(services);
});
});

it('should return service bindings for functions & packages', () => {
const service = openwhiskCompileServiceBindings.serverless.service;
service.resources.packages = {
a: { bind: [{ service: { name: 'service-name-a' } }] }
};

const fns = {
b: { bind: [{ service: { name: 'service-name-b', instance: 'instance-name' } }] },
}

service.getAllFunctions = () => Object.keys(fns)
service.getFunction = name => fns[name]

const services = [
{ action: 'serviceName_a', name: 'service-name-a' },
{ action: 'serviceName_b', name: 'service-name-b', instance: 'instance-name' }
]

return openwhiskCompileServiceBindings.compileServiceBindings().then(() => {
expect(service.bindings).to.deep.equal(services);
});

})
})
});
5 changes: 4 additions & 1 deletion deploy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const deployRules = require('./lib/deployRules');
const deployTriggers = require('./lib/deployTriggers');
const deployFeeds = require('./lib/deployFeeds');
const deployApiGw = require('./lib/deployApiGw');
const deployServiceBindings = require('./lib/deployServiceBindings');

class OpenWhiskDeploy {
constructor(serverless, options) {
Expand All @@ -23,7 +24,8 @@ class OpenWhiskDeploy {
deployApiGw,
deployRules,
deployTriggers,
deployFeeds
deployFeeds,
deployServiceBindings
);

this.hooks = {
Expand All @@ -37,6 +39,7 @@ class OpenWhiskDeploy {
.then(this.deployTriggers)
.then(this.deployFeeds)
.then(this.deployRules)
.then(this.configureServiceBindings)
.then(() => this.serverless.cli.log('Deployment successful!')),
};
}
Expand Down
Loading

0 comments on commit 7ffcbef

Please sign in to comment.