Repository: serverless/serverless (JavaScript)
Date: 2017-06-26 15:10:51+00:00
Task type: Bug Fix

Issue:
serverless deploy function ignores package artifact
# This is a Bug Report

## Description
### What went wrong?
Consider the below serverless.yml file
```
service: test-service

provider:
  name: aws
  runtime: python2.7

functions:
  hello:
    handler: handler.hello
    package:
      artifact: upload.zip
```

When `serverless deploy function -f hello` is run, Serverless ignores the `functions.hello.package.artifact` configuration and falls back to whatever exclude/include configuration it determines.
This indicates that `serverless deploy function -f` is currently ignoring the `functions.[function name].package.artifact` config  
### What did you expect should have happened?
I expected `serverless deploy function -f` to respect the `functions.[function name].package.artifact` configuration the same way `serverless deploy` does.
 
### What was the config you used?
See description

### What stacktrace or error message from your provider did you see?
No error but the behavior is inconsistent with expectations

## Additional Data

* ***Serverless Framework Version you're using***: 1.12.1
* ***Operating System***: OS X El Capitan
* ***Stack Trace***:
* ***Provider Error messages***:



Issue discussion:
@ubaniabalogun `package.artifact` is a property for `sls deploy` only, mainly because you can generate a package with `sls package`, but since there's no `sls package function` command, `sls deploy function` is an independent command that packages and deploys the function.

I understand the confusion though 😊 , I think we better add a note to the docs about this, or maybe add that `sls package function` command? do you feel a need for it? would love your feedback on that one! 🙌 
@eahefnawy Thanks for the response. Let me give you more insight into the problem I'm trying to solve. Perhaps there's already a solution for this in Serverless.

I currently have my own build process (written as a serverless plugin) that creates my function artifacts when `sls deploy` is run. It works beautifully when my intention is to rebuild/redeploy all my functions.

However, I'd like to avoid rebuilding & redeploying all of my functions after making a slight change to just one.

I imagined `sls deploy function` would provide the opportunity to trigger my build process for just one function, but docs have made it clear that `sls deploy function` is better suited for development.

What options (or hooks) are available in the platform to trigger deployment for a single function? I'm working on a project that, at some point in time, will have north of 100 lambda functions in it, so cutting down deployment time is important.

@ubaniabalogun if you're doing your own custom packaging with a plugin, I think you need to update it to package individual functions too? In that case you'd be hooking into the `sls deploy function` hooks. I don't think `sls package function` command would help you in that case because you're doing your own custom packaging anyway and not even using `sls package` command.

All the best! 🙌 
@eahefnawy 
> if you're doing your own custom packaging with a plugin, I think you need to update it to package individual functions too? In that case you'd be hooking into the sls deploy function hooks. 

This is exactly how this bug was detected and why I created this bug report.

When I tried to hook into `sls deploy function` hooks, I discovered `sls deploy function` ignores the value in `functions.[function name].package.artifact`, and obeys only exclude/include rules. This makes it impossible to use a build process with the current implementation of `sls deploy function`

I'll update the issue report to reflect that `functions.[function name].package.artifact` is what is being ignored by `sls deploy function`. The current behavior seems like a notable deviation from the current deployment workflow

Solution patch:
diff --git a/lib/plugins/aws/deploy/lib/extendedValidate.js b/lib/plugins/aws/deploy/lib/extendedValidate.js
index 595e7ca6b5d..965d91b3197 100644
--- a/lib/plugins/aws/deploy/lib/extendedValidate.js
+++ b/lib/plugins/aws/deploy/lib/extendedValidate.js
@@ -33,8 +33,16 @@ module.exports = {
         this.serverless.service.package.individually) {
       // artifact file validation (multiple function artifacts)
       this.serverless.service.getAllFunctions().forEach(functionName => {
-        const artifactFileName = this.provider.naming.getFunctionArtifactName(functionName);
-        const artifactFilePath = path.join(this.packagePath, artifactFileName);
+        let artifactFileName = this.provider.naming.getFunctionArtifactName(functionName);
+        let artifactFilePath = path.join(this.packagePath, artifactFileName);
+
+        // check if an artifact is used in function package level
+        const functionObject = this.serverless.service.getFunction(functionName);
+        if (_.has(functionObject, ['package', 'artifact'])) {
+          artifactFilePath = functionObject.package.artifact;
+          artifactFileName = path.basename(artifactFilePath);
+        }
+
         if (!this.serverless.utils.fileExistsSync(artifactFilePath)) {
           throw new this.serverless.classes
             .Error(`No ${artifactFileName} file found in the package path you provided.`);
diff --git a/lib/plugins/aws/deployFunction/index.js b/lib/plugins/aws/deployFunction/index.js
index 7daecf754bd..836e7a964e1 100644
--- a/lib/plugins/aws/deployFunction/index.js
+++ b/lib/plugins/aws/deployFunction/index.js
@@ -1,6 +1,7 @@
 'use strict';
 
 const BbPromise = require('bluebird');
+const _ = require('lodash');
 const crypto = require('crypto');
 const path = require('path');
 const fs = require('fs');
@@ -68,7 +69,14 @@ class AwsDeployFunction {
   deployFunction() {
     const artifactFileName = this.provider.naming
       .getFunctionArtifactName(this.options.function);
-    const artifactFilePath = path.join(this.packagePath, artifactFileName);
+    let artifactFilePath = path.join(this.packagePath, artifactFileName);
+
+    // check if an artifact is used in function package level
+    const functionObject = this.serverless.service.getFunction(this.options.function);
+    if (_.has(functionObject, ['package', 'artifact'])) {
+      artifactFilePath = functionObject.package.artifact;
+    }
+
     const data = fs.readFileSync(artifactFilePath);
 
     const remoteHash = this.serverless.service.provider.remoteFunctionData.Configuration.CodeSha256;
diff --git a/lib/plugins/package/lib/packageService.js b/lib/plugins/package/lib/packageService.js
index f6d3681b705..049ea1b585f 100644
--- a/lib/plugins/package/lib/packageService.js
+++ b/lib/plugins/package/lib/packageService.js
@@ -1,6 +1,7 @@
 'use strict';
 
 const BbPromise = require('bluebird');
+const path = require('path');
 const _ = require('lodash');
 
 module.exports = {
@@ -70,12 +71,21 @@ module.exports = {
     const functionObject = this.serverless.service.getFunction(functionName);
     const funcPackageConfig = functionObject.package || {};
 
+    // use the artifact in function config if provided
+    if (funcPackageConfig.artifact) {
+      const filePath = path.join(this.serverless.config.servicePath, funcPackageConfig.artifact);
+      functionObject.package.artifact = filePath;
+      return BbPromise.resolve(filePath);
+    }
+
     const exclude = this.getExcludes(funcPackageConfig.exclude);
     const include = this.getIncludes(funcPackageConfig.include);
     const zipFileName = `${functionName}.zip`;
 
     return this.zipService(exclude, include, zipFileName).then(artifactPath => {
-      functionObject.artifact = artifactPath;
+      functionObject.package = {
+        artifact: artifactPath,
+      };
       return artifactPath;
     });
   },


Test patch:
diff --git a/lib/plugins/aws/deploy/lib/extendedValidate.test.js b/lib/plugins/aws/deploy/lib/extendedValidate.test.js
index 5015fb12a76..f253259950d 100644
--- a/lib/plugins/aws/deploy/lib/extendedValidate.test.js
+++ b/lib/plugins/aws/deploy/lib/extendedValidate.test.js
@@ -46,57 +46,85 @@ describe('extendedValidate', () => {
   });
 
   describe('extendedValidate()', () => {
+    let fileExistsSyncStub;
+    let readFileSyncStub;
+
+    beforeEach(() => {
+      fileExistsSyncStub = sinon
+        .stub(awsDeploy.serverless.utils, 'fileExistsSync');
+      readFileSyncStub = sinon
+        .stub(awsDeploy.serverless.utils, 'readFileSync');
+      awsDeploy.serverless.service.package.individually = false;
+    });
+
+    afterEach(() => {
+      awsDeploy.serverless.utils.fileExistsSync.restore();
+      awsDeploy.serverless.utils.readFileSync.restore();
+    });
+
     it('should throw error if state file does not exist', () => {
-      sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync').returns(false);
+      fileExistsSyncStub.returns(false);
+
       expect(() => awsDeploy.extendedValidate()).to.throw(Error);
-      awsDeploy.serverless.utils.fileExistsSync.restore();
     });
 
     it('should throw error if packaged individually but functions packages do not exist', () => {
-      const fileExistsSyncStub = sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync');
       fileExistsSyncStub.onCall(0).returns(true);
       fileExistsSyncStub.onCall(1).returns(false);
-      sinon.stub(awsDeploy.serverless.utils, 'readFileSync').returns(stateFileMock);
+      readFileSyncStub.returns(stateFileMock);
+
       awsDeploy.serverless.service.package.individually = true;
+
       expect(() => awsDeploy.extendedValidate()).to.throw(Error);
-      awsDeploy.serverless.service.package.individually = false;
-      awsDeploy.serverless.utils.fileExistsSync.restore();
-      awsDeploy.serverless.utils.readFileSync.restore();
     });
 
     it('should throw error if service package does not exist', () => {
-      const fileExistsSyncStub = sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync');
       fileExistsSyncStub.onCall(0).returns(true);
       fileExistsSyncStub.onCall(1).returns(false);
-      sinon.stub(awsDeploy.serverless.utils, 'readFileSync').returns(stateFileMock);
+      readFileSyncStub.returns(stateFileMock);
+
       expect(() => awsDeploy.extendedValidate()).to.throw(Error);
-      awsDeploy.serverless.utils.fileExistsSync.restore();
-      awsDeploy.serverless.utils.readFileSync.restore();
     });
 
-    it('should not throw error if service has no functions and no service package available', () => { // eslint-disable-line max-len
-      const functionsTmp = stateFileMock.service.functions;
+    it('should not throw error if service has no functions and no service package', () => {
       stateFileMock.service.functions = {};
-      sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync').returns(true);
-      sinon.stub(awsDeploy.serverless.utils, 'readFileSync').returns(stateFileMock);
+      fileExistsSyncStub.returns(true);
+      readFileSyncStub.returns(stateFileMock);
+
       return awsDeploy.extendedValidate().then(() => {
-        stateFileMock.service.functions = functionsTmp;
-        awsDeploy.serverless.utils.fileExistsSync.restore();
-        awsDeploy.serverless.utils.readFileSync.restore();
+        expect(fileExistsSyncStub.calledOnce).to.equal(true);
+        expect(readFileSyncStub.calledOnce).to.equal(true);
       });
     });
 
-    it('should not throw error if service has no functions and no function packages available', () => { // eslint-disable-line max-len
-      const functionsTmp = stateFileMock.service.functions;
+    it('should not throw error if service has no functions and no function packages', () => {
       stateFileMock.service.functions = {};
       awsDeploy.serverless.service.package.individually = true;
-      sinon.stub(awsDeploy.serverless.utils, 'fileExistsSync').returns(true);
-      sinon.stub(awsDeploy.serverless.utils, 'readFileSync').returns(stateFileMock);
+      fileExistsSyncStub.returns(true);
+      readFileSyncStub.returns(stateFileMock);
+
+      return awsDeploy.extendedValidate().then(() => {
+        expect(fileExistsSyncStub.calledOnce).to.equal(true);
+        expect(readFileSyncStub.calledOnce).to.equal(true);
+      });
+    });
+
+    it('should use function package level artifact when provided', () => {
+      stateFileMock.service.functions = {
+        first: {
+          package: {
+            artifact: 'artifact.zip',
+          },
+        },
+      };
+      awsDeploy.serverless.service.package.individually = true;
+      fileExistsSyncStub.returns(true);
+      readFileSyncStub.returns(stateFileMock);
+
       return awsDeploy.extendedValidate().then(() => {
-        awsDeploy.serverless.service.package.individually = false;
-        stateFileMock.service.functions = functionsTmp;
-        awsDeploy.serverless.utils.fileExistsSync.restore();
-        awsDeploy.serverless.utils.readFileSync.restore();
+        expect(fileExistsSyncStub.calledTwice).to.equal(true);
+        expect(readFileSyncStub.calledOnce).to.equal(true);
+        expect(fileExistsSyncStub).to.have.been.calledWithExactly('artifact.zip');
       });
     });
   });
diff --git a/lib/plugins/aws/deployFunction/index.test.js b/lib/plugins/aws/deployFunction/index.test.js
index eb2f3889ccc..ca90d43d83b 100644
--- a/lib/plugins/aws/deployFunction/index.test.js
+++ b/lib/plugins/aws/deployFunction/index.test.js
@@ -221,5 +221,42 @@ describe('AwsDeployFunction', () => {
         expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true);
       });
     });
+
+    describe('when artifact is provided', () => {
+      let getFunctionStub;
+      const artifactZipFile = 'artifact.zip';
+
+      beforeEach(() => {
+        getFunctionStub = sinon.stub(serverless.service, 'getFunction').returns({
+          handler: true,
+          package: {
+            artifact: artifactZipFile,
+          },
+        });
+      });
+
+      afterEach(() => {
+        serverless.service.getFunction.restore();
+      });
+
+      it('should read the provided artifact', () => awsDeployFunction.deployFunction().then(() => {
+        const data = fs.readFileSync(artifactZipFile);
+
+        expect(readFileSyncStub).to.have.been.calledWithExactly(artifactZipFile);
+        expect(statSyncStub).to.have.been.calledWithExactly(artifactZipFile);
+        expect(getFunctionStub).to.have.been.calledWithExactly('first');
+        expect(updateFunctionCodeStub.calledOnce).to.equal(true);
+        expect(updateFunctionCodeStub.calledWithExactly(
+          'Lambda',
+          'updateFunctionCode',
+          {
+            FunctionName: 'first',
+            ZipFile: data,
+          },
+          awsDeployFunction.options.stage,
+          awsDeployFunction.options.region
+        )).to.be.equal(true);
+      }));
+    });
   });
 });
diff --git a/lib/plugins/package/lib/packageService.test.js b/lib/plugins/package/lib/packageService.test.js
index 4fae5d2cecf..8dd8c531cdd 100644
--- a/lib/plugins/package/lib/packageService.test.js
+++ b/lib/plugins/package/lib/packageService.test.js
@@ -251,5 +251,27 @@ describe('#packageService()', () => {
         ),
       ]));
     });
+
+    it('should return function artifact file path', () => {
+      const servicePath = 'test';
+      const funcName = 'test-func';
+
+      serverless.config.servicePath = servicePath;
+      serverless.service.functions = {};
+      serverless.service.functions[funcName] = {
+        name: `test-proj-${funcName}`,
+        package: {
+          artifact: 'artifact.zip',
+        },
+      };
+
+      return expect(packagePlugin.packageFunction(funcName)).to.eventually
+        .equal('test/artifact.zip')
+        .then(() => BbPromise.all([
+          expect(getExcludesStub).to.not.have.been.called,
+          expect(getIncludesStub).to.not.have.been.called,
+          expect(zipServiceStub).to.not.have.been.called,
+        ]));
+    });
   });
 });