Skip to content

Commit 8859c46

Browse files
committed
fix(deploy): pass gcloud arguments as an array instead of a joined string
spawnAsync built the gcloud command as a single template-literal string and split it on whitespace before handing it to spawn(). Any deploy option containing a space (region, firebaseProject, functionName, cloudRunOptions.vpcConnector, none of which have a schema pattern) would be split into extra argv entries, letting a value from angular.json add unintended flags to the gcloud builds submit / run deploy / auth activate-service-account invocations. spawnAsync now takes command and args separately, matching child_process spawn's own signature, and the three call sites build their argument lists as arrays instead of interpolating into one string. This removes the join/split round-trip entirely rather than trying to validate each field.
1 parent f54c0fe commit 8859c46

1 file changed

Lines changed: 26 additions & 14 deletions

File tree

src/schematics/deploy/actions.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ const DEFAULT_CLOUD_RUN_OPTIONS: Partial<CloudRunOptions> = {
3232

3333
const spawnAsync = async (
3434
command: string,
35+
args: string[],
3536
options?: SpawnOptionsWithoutStdio
3637
) =>
3738
new Promise<Buffer>((resolve, reject) => {
38-
const [spawnCommand, ...args] = command.split(/\s+/);
39-
const spawnProcess = spawn(spawnCommand, args, options);
39+
const spawnProcess = spawn(command, args, options);
4040
const chunks: Buffer[] = [];
4141
const errorChunks: Buffer[] = [];
4242
spawnProcess.stdout.on('data', (data) => {
@@ -353,25 +353,37 @@ export const deployToCloudRun = async (
353353
throw new SchematicsException('Cloud Run preview not supported.');
354354
}
355355

356-
const deployArguments: any[] = [];
356+
const deployArguments: string[] = [];
357357
const cloudRunOptions = options.cloudRunOptions || {};
358358
Object.entries(DEFAULT_CLOUD_RUN_OPTIONS).forEach(([k, v]) => {
359359
cloudRunOptions[k] ||= v;
360360
});
361361
// lean on the schema for validation (rather than sanitize)
362-
if (cloudRunOptions.cpus) { deployArguments.push('--cpu', cloudRunOptions.cpus); }
363-
if (cloudRunOptions.maxConcurrency) { deployArguments.push('--concurrency', cloudRunOptions.maxConcurrency); }
364-
if (cloudRunOptions.maxInstances) { deployArguments.push('--max-instances', cloudRunOptions.maxInstances); }
365-
if (cloudRunOptions.memory) { deployArguments.push('--memory', cloudRunOptions.memory); }
366-
if (cloudRunOptions.minInstances) { deployArguments.push('--min-instances', cloudRunOptions.minInstances); }
367-
if (cloudRunOptions.timeout) { deployArguments.push('--timeout', cloudRunOptions.timeout); }
362+
if (cloudRunOptions.cpus) { deployArguments.push('--cpu', cloudRunOptions.cpus.toString()); }
363+
if (cloudRunOptions.maxConcurrency) { deployArguments.push('--concurrency', cloudRunOptions.maxConcurrency.toString()); }
364+
if (cloudRunOptions.maxInstances) { deployArguments.push('--max-instances', cloudRunOptions.maxInstances.toString()); }
365+
if (cloudRunOptions.memory) { deployArguments.push('--memory', cloudRunOptions.memory.toString()); }
366+
if (cloudRunOptions.minInstances) { deployArguments.push('--min-instances', cloudRunOptions.minInstances.toString()); }
367+
if (cloudRunOptions.timeout) { deployArguments.push('--timeout', cloudRunOptions.timeout.toString()); }
368368
if (cloudRunOptions.vpcConnector) { deployArguments.push('--vpc-connector', cloudRunOptions.vpcConnector); }
369369

370-
// TODO validate serviceId, firebaseProject, and vpcConnector both to limit errors and opp for injection
371-
372370
context.logger.info(`📦 Deploying to Cloud Run`);
373-
await spawnAsync(`gcloud builds submit ${cloudRunOut} --tag gcr.io/${options.firebaseProject}/${serviceId} --project ${options.firebaseProject} --quiet`);
374-
await spawnAsync(`gcloud run deploy ${serviceId} --image gcr.io/${options.firebaseProject}/${serviceId} --project ${options.firebaseProject} ${deployArguments.join(' ')} --platform managed --allow-unauthenticated --region=${options.region} --quiet`);
371+
await spawnAsync('gcloud', [
372+
'builds', 'submit', cloudRunOut,
373+
'--tag', `gcr.io/${options.firebaseProject}/${serviceId}`,
374+
'--project', options.firebaseProject,
375+
'--quiet',
376+
]);
377+
await spawnAsync('gcloud', [
378+
'run', 'deploy', serviceId,
379+
'--image', `gcr.io/${options.firebaseProject}/${serviceId}`,
380+
'--project', options.firebaseProject,
381+
...deployArguments,
382+
'--platform', 'managed',
383+
'--allow-unauthenticated',
384+
'--region', options.region,
385+
'--quiet',
386+
]);
375387

376388
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
377389
const siteTarget = options.target ?? context.target!.project;
@@ -405,7 +417,7 @@ export default async function deploy(
405417
}
406418

407419
if (!firebaseToken && process.env.GOOGLE_APPLICATION_CREDENTIALS) {
408-
await spawnAsync(`gcloud auth activate-service-account --key-file ${process.env.GOOGLE_APPLICATION_CREDENTIALS}`);
420+
await spawnAsync('gcloud', ['auth', 'activate-service-account', '--key-file', process.env.GOOGLE_APPLICATION_CREDENTIALS as string]);
409421
console.log(`Using Google Application Credentials.`);
410422
}
411423

0 commit comments

Comments
 (0)