-
Notifications
You must be signed in to change notification settings - Fork 409
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
Conversation
@@ -61,6 +62,7 @@ | |||
"mocha-multi-reporters": "^1.1.7", | |||
"mock-spawn": "0.2.6", | |||
"nyc": "^13", | |||
"proxyquire": "2.1.3", |
There was a problem hiding this comment.
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" | ||
}, |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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'; | ||
|
There was a problem hiding this comment.
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 ( |
There was a problem hiding this comment.
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< |
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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< |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 @@ | |||
|
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.'; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | |||
); |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(() => { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 | ||
} | ||
} | ||
)); |
There was a problem hiding this comment.
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); | ||
}); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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<{ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
packages/salesforcedx-vscode-core/src/commands/functions/functionService.ts
Show resolved
Hide resolved
} | ||
} | ||
); | ||
} catch (error) { |
There was a problem hiding this comment.
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.', () => { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
* 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
* 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
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.