-
Notifications
You must be signed in to change notification settings - Fork 1k
Add support for deploying local source to App Hosting #8516
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
Conversation
some changes from https://github.com/firebase/firebase-tools/pull/8479/files are showing up in this PR I'm happy to do a fast review of https://github.com/firebase/firebase-tools/pull/8479/files after comments have been resolved so you can merge and get those changes rebased into this PR meanwhile will try an initial review using https://github.com/firebase/firebase-tools/compare/b5697e6209372606c2692bafe253c0fc9f9280dd..fdc6255ec928bac4a3871572793d856703674a06?diff=split&w |
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.
initial review of just the first few files in https://github.com/firebase/firebase-tools/compare/b5697e6209372606c2692bafe253c0fc9f9280dd..fdc6255ec928bac4a3871572793d856703674a06?diff=split&w
I skimmed the others. Could you just pull out src/gcp/apphosting.ts, src/gcp/devConnect.ts, and src/gcp/storage.ts changes into other PRs, and I can take a quick look and then stamp, before continuing with this PR
@@ -250,12 +251,31 @@ export async function ensureAppHostingComputeServiceAccount( | |||
); | |||
} | |||
} | |||
if (deployFromSource) { |
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.
Can you add a comment in the code explaining why this new roles/storage.objectViewer stuff is necessary
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.
^ for better code comprehension e.g. letting reader know that deploy from source has the source code bundled in storage buckets so we need access to that bucket.
but also, now I see below that roles/storage.objectViewer
is also added in provisionDefaultComputeServiceAccount() below, which is called in this ensureAppHostingComputeServiceAccount()
and I'm not sure why?
either there's a better way to do it to make things more clear natively or let's document what we're doing and explain any weird/confusing stuff.
@@ -23,6 +23,7 @@ export const VALID_DEPLOY_TARGETS = [ | |||
"remoteconfig", | |||
"extensions", | |||
"dataconnect", | |||
"apphosting", | |||
]; | |||
export const TARGET_PERMISSIONS: Record<(typeof VALID_DEPLOY_TARGETS)[number], 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.
does apphosting require any permissions we need to check for?
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.
added calls with ensureApiEnabled
to check if App Hosting API is enabled. Double checking with team which specific IAM permissions are required
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.
reviewed up to src/deploy/apphosting/deploy.ts
src/deploy/apphosting/deploy.ts
Outdated
} | ||
|
||
// Ensure that a bucket exists in each region that a backend is or will be deployed to | ||
const backendLocations = context.backendLocations.values() || []; |
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.
if context.backendLocations is empty, i would expect .values() to just return [], so we don't need the || []
?
also nit: can we inline context.backendLocations.values()
into the for loop as well?
src/deploy/apphosting/deploy.ts
Outdated
try { | ||
await gcs.createBucket(projectId, { | ||
name: bucketName, | ||
location: "US-CENTRAL1", |
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.
does the bucket location also need to match the apphosting location? or are we just creating a bucket for each apphosting location?
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.
We're creating a bucket on demand for every location that has an deploy-from-source backend in it.
For example: the user is trying to deploy from source backend A in us-central1, but there's no bucket yet in us-central1 so we create it. There is also a backend B in asia-east1, but the user hasn't tried to deploy B from source yet, so we don't create that bucket.
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 should this line be:
location: loc
not:
location: "US-CENTRAL1"
src/deploy/apphosting/prepare.ts
Outdated
if (options.force) { | ||
throw new FirebaseError( | ||
`Failed to deploy in non-interactive mode: backend ${cfg.backendId} does not exist yet, ` + | ||
"and we cannot create one for you because you must choose a primary region for the backend. " + | ||
"Please run 'firebase apphosting:backends:create' to create the backend, then retry deployment.", | ||
); | ||
} | ||
logBullet(`No backend '${cfg.backendId}' found. Creating a new backend...`); | ||
const location = await promptLocation( | ||
projectId, | ||
"Select a primary region to host your backend:\n", | ||
); | ||
const webApp = await webApps.getOrCreateWebApp(projectId, null, cfg.backendId); | ||
if (!webApp) { | ||
logWarning(`Firebase web app not set`); | ||
} | ||
const createBackendSpinner = ora("Creating your new backend...").start(); | ||
const backend = await createBackend( | ||
projectId, | ||
location, | ||
cfg.backendId, | ||
null, | ||
undefined, | ||
webApp?.id, | ||
); | ||
createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`); | ||
context.backendConfigs.set(cfg.backendId, cfg); | ||
context.backendLocations.set(cfg.backendId, location); |
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.
Can you pull this into some separate doSetupSourceDeploy()
function that lives in https://github.com/firebase/firebase-tools/blob/master/src/apphosting/backend.ts
This has a too much repeat code from deSetup(): https://github.com/firebase/firebase-tools/blob/master/src/apphosting/backend.ts#L74
if we don't have time to refactor and reduce the duplicate code here, these should at least live near each other, so we're more likely to dedupe in the future and we're more likely to change things in both places and keep the UX behaviors consistent.
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.
Also it seems like the big block of code above
await Promise.all([
ensure(projectId, developerConnectOrigin(), "apphosting", true),
ensure(projectId, cloudbuildOrigin(), "apphosting", true),
ensure(projectId, secretManagerOrigin(), "apphosting", true),
ensure(projectId, cloudRunApiOrigin(), "apphosting", true),
ensure(projectId, artifactRegistryDomain(), "apphosting", true),
ensure(projectId, iamOrigin(), "apphosting", true),
]);
await ensureAppHostingComputeServiceAccount(
projectId,
/* serviceAccount= */ null,
/* deployFromSource= */ true,
);
should go into this new doSetupSourceDeploy()
, since it's also in doSetup()
and all these don't need to be made when the backend already exists?
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 repeated below:
context.backendConfigs.set(cfg.backendId, cfg);
context.backendLocations.set(cfg.backendId, location);
can you move this to the bottom of the for loop
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 see your point about having the create logic in the same module, I've refactored it into a doSetupSourceDeploy() function in https://github.com/firebase/firebase-tools/blob/master/src/apphosting/backend.ts. However, I think de-duping might be a bit premature; refactoring the duplicate logic into a helper function would require introducing new arguments to that helper (i.e. we would need an optional backendId
arg since source deploys won't need to prompt for a backend ID, and same thing for webAppName
), just to avoid repeating three function calls. So as of now, from a readability perspective I'm leaning on the side of keeping separate doSetup and doSetupSourceDeploy. But as the logic expands in the future, I'm open to revisiting the refactor option. WDYT?
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.
yeah keeping similar code close together, so anyone making changes in one is more likely to not miss making the same changes in the other is my main concern. agree deduping would be a big task right now that can be left for later given timeline.
src/deploy/apphosting/prepare.ts
Outdated
} | ||
if (selector.startsWith("apphosting:")) { | ||
const backendId = selector.replace("apphosting:", ""); | ||
if (selector.length > 0) { |
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.
Should this be if (backendId.length > 0)
?
src/deploy/apphosting/prepare.ts
Outdated
for (const id of backendIds) { | ||
const cfg = backendConfigs.find((cfg) => cfg.backendId === id); | ||
if (cfg) { | ||
filtered.push(cfg); | ||
} | ||
} |
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.
feel like this could all be replaced by
filtered = backendConfigs.filter(cfg => backendIds.include(cfg.BackendId));
src/deploy/apphosting/prepare.ts
Outdated
if (cfg.alwaysDeployFromSource === false) { | ||
continue; | ||
} |
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.
nit: move this above defining the two constants, since you don't need them.
for readability, delay creating new variables until right before they're first needed.
src/deploy/apphosting/prepare.ts
Outdated
if (!confirmDeploy) { | ||
logWarning(`Skipping deployment of backend ${cfg.backendId}`); | ||
continue; | ||
} |
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 confirmDeploy
could ever be falsey except within the if (!options.force) {
if statement. So i think we can keep confirmDeploy
and the logwarning all inside the if statement and get rid of this extra else
src/deploy/apphosting/release.ts
Outdated
const multipleRolloutsMessage = `Starting new rollouts for backends ${Array.from(context.backendConfigs.keys()).join(", ")}`; | ||
const singleRolloutMessage = `Starting a new rollout for backend ${Array.from(context.backendConfigs.keys()).join(", ")}`; | ||
logBullet( | ||
`${rollouts.length > 1 ? multipleRolloutsMessage : singleRolloutMessage}; this may take a few minutes. It's safe to exit now.`, | ||
); |
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 just saying "Starting rollout(s) for backend(s) ...." is fine and does not degrade user experience.
ce05795
to
1588252
Compare
src/deploy/apphosting/util.ts
Outdated
ignore.push(...gitIgnorePatterns); | ||
|
||
if (!projectRoot) { | ||
projectRoot = process.cwd(); |
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 we want to let the user known what directory is being used as their projectRoot?
I think it would be a big deal if the user accidentally deploys the wrong set of files, but I don't know if we're preventing / reducing chances of that happening at some point prior to this.
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.
Added some log lines. Users can cancel the upload file stream if they realize they uploaded the wrong files.
src/deploy/apphosting/util.ts
Outdated
crlfDelay: Infinity, | ||
}); | ||
rl.on("line", (line: string) => { | ||
if (line.startsWith("#") || line.trim() === "") { |
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.
nit: would feel better if line was trimmed before checking if it starts with "#" as well.
src/deploy/apphosting/util.ts
Outdated
|
||
async function parseGitIgnorePatterns(filePath = ".gitignore"): Promise<string[]> { | ||
const absoluteFilePath: string = path.resolve(filePath); | ||
const lines: 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.
nit: move lines
to inside promise?
…e feature (#8545) * add gcp api calls to support deploy from source
Gemini is currently not able to handle arbitrary key/value dictionaries, so I had to change some of the schemas around.
src/deploy/apphosting/util.ts
Outdated
return { projectSourcePath: projectRoot, zippedSourcePath: tmpFile }; | ||
} | ||
|
||
async function parseGitIgnorePatterns(filePath = ".gitignore"): Promise<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.
I'd really like us to not write code where we don't have to. but since we're on a time crunch:
could you leave a TODO to say that this could potentially be replaced by
https://www.npmjs.com/package/parse-gitignore
so whoever touches this next can consider doing it.
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.
actually, as bryan said somewhere else, this package essentially does
"fs.ReadFileSync("path").filter(startsWith("#"))"
I'm realizing it's not clear to me why we need 30+ lines of code instead of just
return fs.ReadFileSync(".gitinore").filter( (line) => line.trim().startsWith("#") || line.trim() === ""))
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.
Fair enough. The event listening approach is kind of an old school way to do it. Made the changes
firebase deploy
now deploys source code on the user's local machine to the corresponding backend on App Hosting. Guides the user through backend creation if the backend does not exist yet.Note that
firebase deploy
relies on a newapphosting
section in thefirebase.json
file, which can be initialized using the enhancedfirebase init apphosting
command (implemented here #8479).