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

containerless function when running locally #3838

Merged
merged 23 commits into from
Feb 23, 2022

Conversation

gbockus-sf
Copy link
Contributor

@gbockus-sf gbockus-sf commented Feb 14, 2022

What does this PR do?

Add support for running a function directly on the developers system.
Alters the Start Function command to run locally instead of in a container.
Refactor the start function code to support container & containerless
I'm not sure how to see unit test coverage (yet), but I attempted to get full code coverage for the classes I touched in this PR.

What issues does this PR fix or reference?

@W-10535876@
Also deals with W-10535850

Functionality Before

Previously we defaulted to running functions in the containers running locally on the developer system via the 'SFDX: Start Function' command.

Functionality After

There are now two commands available:
'SFDX: Start Function' Starts the function locally on the developers system outside of the container.
'SFDX: Start Container Function' Starts the function in the container locally on the developers system.

@@ -61,6 +62,7 @@
"mocha-multi-reporters": "^1.1.7",
"mock-spawn": "0.2.6",
"nyc": "^13",
"proxyquire": "2.1.3",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add proxyquire as a dev dep to the core project to enable unit testing outside the integration test flow. We are using this in a few packages already, but didn't have any references in core.

{
"command": "sfdx.force.function.container.start",
"when": "sfdx:project_opened && resource =~ /.*/functions/.*/[^/]+(/[^/]+\\.(ts|js|java|json|toml))?$/ && resourceFilename != package.json && resourceFilename != package-lock.json && resourceFilename != tsconfig.json"
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the previous sfdx.force.function.start was left unaltered here, but is updated to run a new command function in index.ts.

@@ -70,6 +70,7 @@
"force_analytics_template_create_text": "SFDX: Create Sample Analytics Template",
"force_function_create": "SFDX: Create Function",
"force_function_start": "SFDX: Start Function",
"force_function_start_container": "SFDX: Start Container Function",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New command that will run the old container based start function code. @sbudhirajadoc

@@ -0,0 +1,41 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to do a full pass at updating/add copyrights.

* Executes sfdx run:function:start --verbose
* @param sourceUri
*/
export async function forceFunctionContainerStartCommand(sourceUri?: Uri) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did some refactoring here to try and separate some concerns. Previously we have one file that exported both the command and executor. I moved those to different files to enable easier unit testing of each piece of the puzzle.

import { notificationService } from '../../../notifications';
import { FunctionService } from '../functionService';
import { FUNCTION_RUNTIME_DETECTION_PATTERN } from '../types/constants';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll put all these import together.

* Executes sfdx run:function:start:local --verbose
* @param sourceUri
*/
export const forceFunctionContainerlessStartCommand = async (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make this consistent across both function start commands.

}
};

export class ForceFunctionStartExecutor extends LibraryCommandletExecutor<
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored this into a base abstract class that is extended by the container and containerless executors. There was a large chunk of shared code which drove my decision here.

*/
type ForceFunctionStartErrorType = 'force_function_start_docker_plugin_not_installed_or_started';

const forceFunctionStartErrorInfo: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code here is mainly taken from the original executor. I just refactored it into the new abstract methods.

Copy link
Contributor

@jeffb-sfdc jeffb-sfdc Feb 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading through the code, I see what forceFunctionStartErrorInfo is doing, and looks like this code & its approach originally came from forceFunctionStart.ts (and isn't your fault). But reading through the code, my first thought was... "what?"

This (the original code that was carried forward) seems (IMHO) to be a little over engineered. This might be a good opportunity to clean it up and make it more simple. We could get rid of ForceFunctionStartErrorType and forceFunctionStartErrorInfo, and just change it to:

const forceFunctionStartErrorInfo = {
  force_function_start_docker_plugin_not_installed_or_started: {
    cliMessage: 'Cannot connect to the Docker daemon',
    errorNotificationMessage: nls.localize(
      'force_function_start_warning_docker_not_installed_or_not_started'
    )
  }
}

...and then further down, not have to do (Object.keys(forceFunctionStartErrorInfo) as ForceFunctionStartErrorType[]).forEach(...

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt that "what?" I've had to review this a couple of times to understand it and even then it def seem over engineered. I'll simplify. 👍

public async cancelFunction(
registeredStartedFunctionDisposable: Disposable
): Promise<void> {
// TODO: how to stop the localRun
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will need to follow up with a new ticket for how to handle stopping the containerless functions as there is no mechanism for achieving that today.

console.log('Gordon resolved: ' + msg);
})
.catch(err => {
console.log('Gordon Error: ' + err.message);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will address consoles. Wasn't sure what exactly should happen in these cases.

debugPort: FUNCTION_DEFAULT_DEBUG_PORT
});

localRun
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LocalRun has a return type of Promise<void>. So there is not handle to the started function process.

functionName: string,
functionDirPath: string
): Promise<void> {
console.log('No build for containerless function');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll address these consoles in the code.

FUNCTION_DEFAULT_PORT
} from '../types/constants';

export abstract class ForceFunctionStartExecutor extends LibraryCommandletExecutor<
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base class extended by the two executors.

return true;
}

public abstract setupFunctionListeners(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new abstract methods that are implemented by the different flavors of executors.

@@ -0,0 +1,4 @@

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need a copyright.


export {ForceFunctionContainerlessStartExecutor} from './ForceFunctionContainerlessStartExecutor';
export {ForceFunctionContainerStartExecutor} from './ForceFunctionContainerStartExecutor';
export { validateStartFunctionsUri } from './validateStartFunctionsUri';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hum...I'm not sure why prettier didn't catch the space inconsistency in these exports.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think we have a rule for this. But for consistency, I see we use space in both exports and imports.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hum...yeah I double checked and prettier should take care of this. I'm not sure how it slipped by the first time.

@@ -43,6 +52,8 @@ export interface FunctionExecution extends Terminable {
debugSession?: vscode.DebugSession;
}

export const FUNCTION_TYPE_ERROR = 'Unable to determine type of exectucting function.';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbudhirajadoc New error message on invalid function type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo with "exectucting"

@@ -352,7 +353,12 @@ function registerCommands(

const forceFunctionStartCmd = vscode.commands.registerCommand(
'sfdx.force.function.start',
forceFunctionStart
forceFunctionContainerlessStartCommand
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is where the containerless start function is replacing the previous behavior.

@@ -143,7 +143,8 @@ export const messages = {
force_lightning_event_create_text: 'SFDX: Create Aura Event',
force_lightning_interface_create_text: 'SFDX: Create Aura Interface',
force_function_create_text: 'SFDX: Create Function',
force_function_start_text: 'SFDX: Start Function',
force_function_container_start_text: 'SFDX: Start Container Function',
force_function_containerless_start_text: 'SFDX: Start Local Function',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DO I need to do anything with the package.nls.ja.json if new values are added here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not required. We rely on an external contributor (and until then, it defaults to English).

let sfdxWorkspaceCheckerConstructorStub: SinonStub;
let forceFunctionContainerStartExecutorConstructorStub: SinonStub;

beforeEach(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I probably spent entirely too much time trying to figure out how to deal with the vscode module when developing unit tests locally. The issue is it doesn't exist locally so you can't mock it (or even run a test that imports code where it gets referenced) when running the tests outside of vscode (the ide).

I found a work around that I don't love, but found it to be the lesser of the two evils of not writing unit tests.
Using proxyquire I can ensure we are stubbing all dependancies. The downside is I'm losing the type checking when calling the function under test (note this impact the typescript compile for the test only).

const fakeUri: any = 'what';
let validateStartFunctionsUriStub: SinonStub;
let commandletRunStub: SinonStub;
let forceFunctionContainerStartCommand: any;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is our type checking sadness I mentioned below.

validateStartFunctionsUri: validateStartFunctionsUriStub
}
}
));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here is where I pull in the exported members from the forceFunctionContainerStartCommand module. This is done here in order to provide the stubbed dependancies to the class under test.

validateStartFunctionsUriStub.returns(undefined);
await forceFunctionContainerStartCommand(fakeUri);
assert.notCalled(commandletRunStub);
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests are nice and easy, but the stubbing is kind of gnarly

@@ -5,15 +5,15 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rename this to indicate it's an integration test for the container start flow.


public async startFunction(
functionName: string,
functionDirPath: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functionDirPath isn't used. Can it be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call. in the abstract method def the param is optional I can just exclude it (needed for containerless implementation)

if (!this.functionsBinary) {
throw new Error('Unable to start function with no binary.');
}
this.functionsBinary.run(functionName, {}).catch(err => console.log(err));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of putting multiple statements on a single line. (at the very least) it makes placing break points difficult, and (me personally) I prefer:

    this.functionsBinary.run(functionName, {})
      .catch(err => console.log(err));

I'd like others on the team to weigh in. Maybe it's just me, but I prefer the calls to be on seperate lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

me neither! love it. I don't think prettier would let me put it on the next line as is, but I'm totally cool adding the () and {} to the catch handler for exactly the reason you said.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turns out we have a tslint rule for including parans for a single param function. but {} added.

export class ForceFunctionContainerlessStartExecutor extends ForceFunctionStartExecutor {
public async setupFunctionListeners(
functionDirPath: string,
functionDisposable: Disposable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functionDirPath and functionDisposable aren't used. Can they be removed?


public async buildFunction(
functionName: string,
functionDirPath: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functionName and functionDirPath aren't used. Can they be removed?


public async run(
response: ContinueResponse<string>,
progress?: vscode.Progress<{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is progress used, or can it be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it can b/c there is an additional optional parameter (increment) that is used. If we remove message then increment will be undefined when run is called.

this.buildFunction(functionName, functionDirPath);

channelService.appendLine(`Starting ${functionName}`);
await this.startFunction(functionName, functionDirPath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VSC is reporting a warning here that await is't needed. Can it be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 need to figure out why vsc wasn't complaining for me

/**
* An enum for the different types of functions.
*/
export enum FUNCTION_TYPE {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the convention for enums in TS is PascalCase, so:

export enum FunctionType {
  JavaScript,
  TypeScript,
  Java
}

Here's what the TypeScript docs say: https://www.typescriptlang.org/docs/handbook/enums.html

}
}
);
} catch (error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand this line. empty catches only lead to sadness 🤷


const proxyquireStrict = proxyquire.noCallThru();

describe.only('Force Function Start Container Command Unit Tests.', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missed an only 🤦 . fixed.

telemetryService.sendException(this.UNEXPECTED_ERROR_KEY, err.message);
notificationService.showErrorMessage(errorNotificationMessage);
channelService.appendLine(errorNotificationMessage);
if (err?.message) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the exception here isn't nullable, so the ? isn't needed. Either that, or line 55 should be changed to .catch((err: Error | undefined)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 fixed

const localRun = new LocalRun(functionLanguage, {
path: functionDirPath,
port: FUNCTION_DEFAULT_PORT,
debugPort: FUNCTION_DEFAULT_DEBUG_PORT
});

const debugType = functionLanguage === functionType.JAVA ? 'java' : 'node';
FunctionService.instance.updateFunction(functionDirPath, debugType);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in the container function flow this happens in an odd place so I missed it for containerless function. This updates the debug type after the function is started (which is required to run the service method for getting the type.

@jeffb-sfdc jeffb-sfdc merged commit a4f61b9 into develop Feb 23, 2022
@jeffb-sfdc jeffb-sfdc deleted the gbockus/containerless-functions branch February 23, 2022 21:41
jag-j pushed a commit that referenced this pull request Feb 24, 2022
* feat: package update and wiring for containerless funcs

* feat: add flow for containerless func start

* feat: changes from self review

* feat: add copyright to all the new files

* feat: add unit test for ForceFunctionContainerStartExecutor

* feat: lint fixes

* feat: add ForceFunctionContainerlessStartExecutor unit tests

* fix: changes from review

* fix: update FunctionService unit test

* fix: unit test fixes

* fix: fix start comment integration test

* fix: add log for error that occurs on local function start

* fix: changes from review

* fix: allow debugging local java functions
jag-j pushed a commit that referenced this pull request Feb 24, 2022
* feat: package update and wiring for containerless funcs

* feat: add flow for containerless func start

* feat: changes from self review

* feat: add copyright to all the new files

* feat: add unit test for ForceFunctionContainerStartExecutor

* feat: lint fixes

* feat: add ForceFunctionContainerlessStartExecutor unit tests

* fix: changes from review

* fix: update FunctionService unit test

* fix: unit test fixes

* fix: fix start comment integration test

* fix: add log for error that occurs on local function start

* fix: changes from review

* fix: allow debugging local java functions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants