Skip to content

Implementing createDeploymentPlan #3081

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

Merged
merged 9 commits into from
Feb 1, 2021
Merged

Conversation

joehan
Copy link
Contributor

@joehan joehan commented Jan 26, 2021

Description

Implementing createDeploymentPlan, per the design at go/cf3-deployment-refactor. Part of my goal for this refactor is to separates out the logic for what to deploy from how to deploy it, to make the code more testable and easier to change in the future. This PR handles the first part of that, and will followed up by another PR to execute a deployment plan .

Scenarios Tested

In addition to the cases covered by the unit tests, I locally modified the release step to call createDeploymentPlan and print out the results, and manually tested out a bunch of different permutations of function creates, updates, and deletes, with and without schedules and --only flags.

@google-cla google-cla bot added the cla: yes Manual indication that this has passed CLA. label Jan 26, 2021
Copy link
Contributor

@bkendall bkendall left a comment

Choose a reason for hiding this comment

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

Honestly just some nitty things, but looks fine overall. Maybe someone on Functions should definitely check it out before merging though :)

Comment on lines 201 to 206
return _.chain(regionMap)
.map((value: CloudFunctionTrigger[]) => {
return value;
})
.flatten()
.value();
Copy link
Contributor

Choose a reason for hiding this comment

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

What is your feeling about _.chain? Personally, I'm not a fan.

Is this equivalent?

const triggers: CloudFunctionTrigger[] = [];
for (const [k, v] of Object.entries(regionMap)) {
  triggers.push(...v);
}
return triggers;

If you're really wanting to keep _.chain though, here's a simplification you can make:

Suggested change
return _.chain(regionMap)
.map((value: CloudFunctionTrigger[]) => {
return value;
})
.flatten()
.value();
return _.chain(regionMap)
.map((v: CloudFunctionTrigger[]) => v)
.flatten()
.value();

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'm not the biggest fan of _.chain either - its tough to understand what going on with it if you're not familiar with lodash, and its always been unintuitive to me that it doesn't execute any of the chained functions until you call '.value()'.

However, as I'm working on the follow up PR, I've realized that I can get rid of this method entirely with some minor refactoring of checkIam.ts. For now, I think I'll go with the simplified _.chain approach - in a follow up PR, this should be gone entirely.

Copy link
Contributor

Choose a reason for hiding this comment

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

heh - seriously though, my goal is to get rid of lodash entirely. So much of it can be done with plain JS (or a simple helper function), so it's not unattainable...

Copy link
Contributor

Choose a reason for hiding this comment

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

+💯 to getting rid of lodash (especially the more esoteric bits) everywhere we're rewriting code.

Comment on lines 209 to 222
export interface RegionalDeployment {
region: string;
sourceToken?: string;
firstFunctionDeployment?: CloudFunctionTrigger;
functionsToCreate: CloudFunctionTrigger[];
functionsToUpdate: CloudFunctionTrigger[];
schedulesToCreateOrUpdate: CloudFunctionTrigger[];
}

export interface DeploymentPlan {
regionalDeployments: RegionalDeployment[];
functionsToDelete: string[];
schedulesToDelete: string[];
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can all the interfaces in this file be collected at the top? It'll make them easier to find later

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Comment on lines 254 to 256
const matchingExistingFunction = _.find(existingFunctions, (exFn) => {
return exFn.name === fn.name;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const matchingExistingFunction = _.find(existingFunctions, (exFn) => {
return exFn.name === fn.name;
});
const matchingExistingFunction = existingFunctions.find((exFn) => {
return exFn.name === fn.name;
});

(Can you tell my feelings on lodash in general now? haha)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Didn't realize there was a native version of this - much nicer.

});
// Also delete any schedules for functions that we are deleting.
_.forEach(functionsToDelete, (fn) => {
if (_.get(fn, "labels.deployment-scheduled") === "true") {
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 you could do fn.labels?.["deployment-scheduled"] === "true"

(the last thing is ["thing"] because of the kabab-casing)

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 believe you're right - plus, its only written this way because I copied this over from old functions code that was written before optional chaining existed. I definitely prefer using the native option, so I'll switch this over.

Copy link
Contributor

@mbleigh mbleigh left a comment

Choose a reason for hiding this comment

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

I think this all mostly makes sense. I like that we are parsing the triggers into a concrete set of operations in the prepare phase, and I didn't see anything obviously wrong with any of the logic. Some high-level comments:

  1. Please remove lodash wherever possible. We should aim to never use lodash in code we're writing today.
  2. Don't feel beholden to the existing file structure. You're cramming a lot of stuff into functionsDeployHelper.ts which is a pretty poorly named file. Feel free to put things in deploy/functions/... in a more modular fashion if you think it will help clarity.

payload.functions.triggers = getFunctionsInfo(
options.config.get("functions.triggers"),
projectId
payload.functions.regionMap = createFunctionRegionMap(
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe byRegion instead of regionMap would be a more clear name? I keep expecting this to be some kind of region translation map.

// Display a warning and prompt if any functions have failurePolicies.
await promptForFailurePolicies(options, payload.functions.triggers);
// Check what --only filters have been passed in.
context.filters = getFilterGroups(options);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have a sense of when to use payload and when to use context? I vaguely remember our rationale from eons ago but my guess is that it's mostly a distinction without a difference at this point. I think I'd probably bias toward stuffing things in context instead of payload, but 🤷 no strong opinion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Honestly, I don't. From reading through the existing code, I kind of figured out that payload = 'stuff that is being deployed' and context = 'things we want to remember for later in the process', but a lot of the things in the code today blur the line between those. As I go thru and address comments on this, I'll probably follow your advice of using context whenever possible.

if (!filterGroups.length) {
return true;
}
return _.some(filterGroups, (groupChunks) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would love to 🔪 lodash everywhere we can, and I don't actually know what _.some does by sight. Can we rewrite in plain JS?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Almost certainly can get rid of lodash here, this was just some old code that I moved from its original home in release.ts and didn't think to rewrite.

// SDK exports list of regions for each function to be deployed to, need to add a new entry
// to functionsInfo for each region.
// Create a separate CloudFunction for
// each region we deploy a function to
_.forEach(trigger.regions, (region) => {
const triggerDeepCopy = JSON.parse(JSON.stringify(trigger));
Copy link
Contributor

Choose a reason for hiding this comment

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

LOL this is horrible but probably fine.

projectId: string,
parsedTriggers: CloudFunctionTrigger[]
): RegionMap {
const regionMap: RegionMap = {};
_.forEach(parsedTriggers, (trigger) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please move to either for...of or .forEach from lodash. Don't have to do it everywhere, but I'd like us to do it in the places we're touching code.

const isMatchingExisitingFnScheduled =
_.get(matchingExistingFunction, "labels.deployment-scheduled") === "true";
// Check if the local function is a scheduled function
const isScheduled = _.has(fn, "schedule");
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not move this down below where the rest of the schedule handling logic is?

// Delete any remaining existing functions that:
// 1 - Have the deployment-tool: 'firebase-cli' label and
// 2 - Match the --only filters, if any are provided.
const functionsToDelete = _.chain(existingFunctions)
Copy link
Contributor

Choose a reason for hiding this comment

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

👴 bernie voice I am once again asking you to factor out lodash.

// Check if the local function is a scheduled function
const isScheduled = _.has(fn, "schedule");

if (!matchingExistingFunction) {
Copy link
Contributor

Choose a reason for hiding this comment

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

In general can you try to move the const declarations as close to their first use as possible? I think it'd read easier.

it("should put new functions into functionsToCreate", () => {
const regionMap: helper.RegionMap = {
"us-east1": [
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be easier to read these tests if you had a helper to generate the basic schema here.

expect(deploymentPlan).to.deep.equal(expected);
});

it("should delete existing functions not in local code, only if they were deployed via CLI", () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just checking that you didn't do anything to alter the logic that prompts before doing any deletes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope (or at least I didn't intend to). This may have just been a confusing title for the test - basically, this is checking that when there are no --only filters, we delete all existing functions that a - have the deployment-tool:cli-firebase label and b- don't have a matching local function.

import { FirebaseError } from "../../error";
import { promptOnce } from "../../prompt";
import * as utils from "../../utils";

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There another prompt for deleting functions that is currently in release.ts - I'll move that here in next PR which refactors release.ts to execute these deploymentPlans.

@joehan joehan requested review from mbleigh and inlined January 28, 2021 17:15
Copy link
Contributor

@mbleigh mbleigh left a comment

Choose a reason for hiding this comment

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

I think this is looking good. Since this is a big risky change we'll need to make sure we do lots of manual testing, but I'm good to go.

* @param projectId The project in use.
* @param parsedTriggers A list of all CloudFunctions in the deployment.
*/
export function createFunctionsByRegionMap(
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: createFunctionsByRegionMap sounds like you're creating functions and passing a region map...how about groupTriggersByRegion?

(sorry for the naming nits I think there's a lot of subtlety to these)

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 like that - and don't worry about the naming nits, its actually much appreciated. I've always found that this code path is difficult to choose good names for, partly because of semantic satiation of the word function, and partly because of the historical lack of clear typing making everything cloudy.

Copy link
Member

Choose a reason for hiding this comment

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

I would personally go further and drop group. IMO a list comprehension that doesn't modify in-place should be named after the result of the comprehension and the verb of how that comprehension is created. This is the difference between uppercase([]string): void and uppercased([]string): []string

functionsToDelete: [],
schedulesToDelete: [],
};
// eslint-disable-next-line guard-for-in
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 curious how @bkendall feels about dropping this ESLint rule. I can't think of a time when I use for...in against something that isn't a plain object, so I don't feel like I'd ever hit the scenario it's guarding against.

* @param filters The filters, passed in by the user via `--only functions:`
*/
export function createDeploymentPlan(
functionsInSourceByRegion: RegionMap,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason to have this take a RegionMap instead of having the region grouped functions be an implementation detail of creating a deployment plan? Should the signature be instead

export function createDeploymentPlan(
  projectId: string,
  parsedTriggers: CloudFunctionTrigger[],
  existingFunctions: CloudFunctionTrigger[],
  filters: string[][]
): DeploymentPlan {

Copy link
Member

@inlined inlined left a comment

Choose a reason for hiding this comment

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

Lots of style nits. There's some things I don't really like about the codebase like complex nesting, non-descriptive variable/parameter names (e.g. params or payload) and double negatives. I'm going to try to nudge us away from that if possible.

regions?: string[];
}

export interface RegionMap {
Copy link
Member

Choose a reason for hiding this comment

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

nit: I wonder if we're going to have a name collision in the future and wish we saved "region map" for the mapping bewteen regions names and their short code (e.g. "us-centra1" -> "uc1"). Though maybe we'll never need that mapping because we'll always ask the control plane for URLs instead of generating them from a pattern?

Copy link
Contributor Author

@joehan joehan Feb 1, 2021

Choose a reason for hiding this comment

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

Seems reasonable to change up this name - Michael also thought it was unclear. If we end up using this type in the final code, I'll switch it to FunctionsByRegion

firstFunctionDeployment?: CloudFunctionTrigger;
functionsToCreate: CloudFunctionTrigger[];
functionsToUpdate: CloudFunctionTrigger[];
schedulesToCreateOrUpdate: CloudFunctionTrigger[];
Copy link
Member

Choose a reason for hiding this comment

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

nit: schedulesToUpsert would be terser and still use standard CRUD naming.

export interface RegionalDeployment {
region: string;
sourceToken?: string;
firstFunctionDeployment?: CloudFunctionTrigger;
Copy link
Member

Choose a reason for hiding this comment

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

This field could probably use a comment. It's not clear here what the heck it means or why it's important. I'm guessing it's because we have no functions in the region and we want to deploy one function first to provision the TP?

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 explain more over chat on Monday - this is here to take advantage of the sourceToken field in operation metadata, which will let us reuse builds across multiple fns & speed up deploys of many functions.


export interface DeploymentPlan {
regionalDeployments: RegionalDeployment[];
functionsToDelete: string[];
Copy link
Member

Choose a reason for hiding this comment

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

Is there any reason why functions and schedules to delete can't also be part of the RegionalDeployment? Aside from Pub/Sub topics they're regional resources, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Scheduler jobs are technically regional - however, they can only live in the default resource location (aka the region where app engine exists on that project). Because of this, they are effectively global.

FunctionsToDelete is also regional - however, we don't need to wait for the first function in that region to be deployed to fire off the delete calls, so I figured it would be simpler to put them all in one array.

* @param projectId The project in use.
* @param parsedTriggers A list of all CloudFunctions in the deployment.
*/
export function createFunctionsByRegionMap(
Copy link
Member

Choose a reason for hiding this comment

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

I would personally go further and drop group. IMO a list comprehension that doesn't modify in-place should be named after the result of the comprehension and the verb of how that comprehension is created. This is the difference between uppercase([]string): void and uppercased([]string): []string

regionalDeployment.functionsToCreate.push(fn);
} else {
regionalDeployment.functionsToUpdate.push(fn);
existingFunctions = existingFunctions.filter((exFn: CloudFunctionTrigger) => {
Copy link
Member

Choose a reason for hiding this comment

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

I feel uncomfortable with the way we mutate parameters all over this codebase. I'm waiting for that to cause a bad bug somewhere. It's not like we are in CPU bottleneck'd code. Would it hurt us to make copies occasionally?

return !!fn.failurePolicy;
});

if (failurePolicyFunctions.length) {
Copy link
Member

Choose a reason for hiding this comment

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

if (failurePolicyFunctions.length == 0) {
  return;
}

throw new FirebaseError("Pass the --force option to deploy functions with a failure policy", {
exit: 1,
});
} else if (!options.nonInteractive) {
Copy link
Member

Choose a reason for hiding this comment

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

ooof.. I hate else if (! something)

I wonder if it's clearer to have

if (options.force) {
 return;
} 
if (options.nonInteractive) { // I hate that this is a name that includes a built-in negative
  throw
}
prompt

As a side-rant, I really really hate "nonInteractive" because you end up with code that reads "not non interactive" in your head. I might add "options.interactive" just to fix this... 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, that does slightly change the behavior (in the suggested code, --force applies even if we're not in nonInteractive mode), but i think thats actually more correct, so I'm gonna make this change.

Base automatically changed from jh-ts-func-release to jh-functions-refactor February 1, 2021 20:38
@joehan
Copy link
Contributor Author

joehan commented Feb 1, 2021

Wanted to get this merged so that the next PR would be a bit simpler to look at, so I added some TODOs for comments that I thought were valuable but I didn't want to address at this moment.

@joehan joehan merged commit 11956fa into jh-functions-refactor Feb 1, 2021
joehan added a commit that referenced this pull request Feb 16, 2021
* Adding onPoll option to operation-poller (#3046)

* Adds onPoll option

* format and remove only

* improved unit test

* Typescriptify functionsDeployHelper (#3059)

* Typescriptifying and adding CloudFunctionTrigger type

* Using funcName instead of func

* formats

* Typescriptifying gcp.cloudfunctions (#3060)

* Typescriptifying functionsConfig (#3063)

* Typescriptifying deploymentTool (#3061)

* Typescriptifying deploymentTool

* Update src/deploymentTool.ts

Co-authored-by: Michael Bleigh <[email protected]>

Co-authored-by: Michael Bleigh <[email protected]>

* Refactoring prepare stage of functions deploy (#3067)

* refactoring prepare setp of functions deploy to use typescript, check for failure policies, and parse function triggers

* refactoring prepare setp of functions deploy to use typescript, check for failure policies, and parse function triggers

* commenting out functionNamesAreValid

* formats

* refactoring release step of functions deploy to use typescript

* Adding logic to build regional deployments

* Implementing createDeploymentPlan

* First round of PR feedback, removing most usages of lodash

* moving function prompts into their own file

* seperating out a bunch of code from functionsDeployHelper

* Resolves merge conflicts

* refactoring release step of functions deploy to use typescript (#3071)

* Implements core logic of running function deploys

* Typescriptifying prepareFunctionsUpload (#3064)

* Typescriptifying prepareFunctionsUpload, and updating filesize package to get types

* fixing merge conflict

* Implementing createDeploymentPlan (#3081)

* refactoring release step of functions deploy to use typescript

* Adding logic to build regional deployments

* Implementing createDeploymentPlan

* First round of PR feedback, removing most usages of lodash

* moving function prompts into their own file

* seperating out a bunch of code from functionsDeployHelper

* round of pr fixes

* adresses more pr comments, and adds some todos

* cleaning up unused code

* Fixing some things that were broken while merging

* Fixing up the order of wait and close to ensure that queue promsies actually resolve

* Format and clean up typos

* refactoring error handling to be cleaner

* cleaning up extera newlines

* first round of pr fixes

* Readding some changes that I accidenttally wiped out during a merge

* Switching name to id where appropriate

* fixing another bug caused by functionName vs Id

* Refactor functions-delete  (#3110)

* Refactoring functions delete to use tasks, and cleaning up old polling code

* Refactoring functions delete to use tasks, and cleaning up old polling code

* refactoring to use new error handling

* cleanup unused imports

* small style fixes

* Cleaning up error reporting

* Implement validation for changing trigger types, and fixes from bug bash (#3131)

* Implement validation for changing trigger types, and fixes from bug bash

* more specifc error messages for different permutations of trigger types

* fixes package.json

Co-authored-by: Michael Bleigh <[email protected]>
@bkendall bkendall deleted the jh-single-build branch August 4, 2021 19:26
devpeerapong pushed a commit to devpeerapong/firebase-tools that referenced this pull request Dec 14, 2021
* Adding onPoll option to operation-poller (firebase#3046)

* Adds onPoll option

* format and remove only

* improved unit test

* Typescriptify functionsDeployHelper (firebase#3059)

* Typescriptifying and adding CloudFunctionTrigger type

* Using funcName instead of func

* formats

* Typescriptifying gcp.cloudfunctions (firebase#3060)

* Typescriptifying functionsConfig (firebase#3063)

* Typescriptifying deploymentTool (firebase#3061)

* Typescriptifying deploymentTool

* Update src/deploymentTool.ts

Co-authored-by: Michael Bleigh <[email protected]>

Co-authored-by: Michael Bleigh <[email protected]>

* Refactoring prepare stage of functions deploy (firebase#3067)

* refactoring prepare setp of functions deploy to use typescript, check for failure policies, and parse function triggers

* refactoring prepare setp of functions deploy to use typescript, check for failure policies, and parse function triggers

* commenting out functionNamesAreValid

* formats

* refactoring release step of functions deploy to use typescript

* Adding logic to build regional deployments

* Implementing createDeploymentPlan

* First round of PR feedback, removing most usages of lodash

* moving function prompts into their own file

* seperating out a bunch of code from functionsDeployHelper

* Resolves merge conflicts

* refactoring release step of functions deploy to use typescript (firebase#3071)

* Implements core logic of running function deploys

* Typescriptifying prepareFunctionsUpload (firebase#3064)

* Typescriptifying prepareFunctionsUpload, and updating filesize package to get types

* fixing merge conflict

* Implementing createDeploymentPlan (firebase#3081)

* refactoring release step of functions deploy to use typescript

* Adding logic to build regional deployments

* Implementing createDeploymentPlan

* First round of PR feedback, removing most usages of lodash

* moving function prompts into their own file

* seperating out a bunch of code from functionsDeployHelper

* round of pr fixes

* adresses more pr comments, and adds some todos

* cleaning up unused code

* Fixing some things that were broken while merging

* Fixing up the order of wait and close to ensure that queue promsies actually resolve

* Format and clean up typos

* refactoring error handling to be cleaner

* cleaning up extera newlines

* first round of pr fixes

* Readding some changes that I accidenttally wiped out during a merge

* Switching name to id where appropriate

* fixing another bug caused by functionName vs Id

* Refactor functions-delete  (firebase#3110)

* Refactoring functions delete to use tasks, and cleaning up old polling code

* Refactoring functions delete to use tasks, and cleaning up old polling code

* refactoring to use new error handling

* cleanup unused imports

* small style fixes

* Cleaning up error reporting

* Implement validation for changing trigger types, and fixes from bug bash (firebase#3131)

* Implement validation for changing trigger types, and fixes from bug bash

* more specifc error messages for different permutations of trigger types

* fixes package.json

Co-authored-by: Michael Bleigh <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes Manual indication that this has passed CLA.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants