Skip to content

Commit 80be005

Browse files
committed
Enable Secret Manager when function includes secrets.
1 parent d9c52e1 commit 80be005

File tree

3 files changed

+144
-26
lines changed

3 files changed

+144
-26
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Fixed issue where `firebase init firestore` would raise an error due to rules/indexes file path being undefined. (#8518)
2+
- Fixed issue where Secret Manager API was not automatically enabled for functions using secrets. (#8528)

src/deploy/functions/prepare.spec.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { expect } from "chai";
22

33
import * as backend from "./backend";
44
import * as prepare from "./prepare";
5+
import * as ensureApiEnabled from "../../ensureApiEnabled";
6+
import * as serviceusage from "../../gcp/serviceusage";
57
import { BEFORE_CREATE_EVENT, BEFORE_SIGN_IN_EVENT } from "../../functions/events/v1";
68
import * as sinon from "sinon";
79
import * as prompt from "../../prompt";
@@ -419,4 +421,96 @@ describe("prepare", () => {
419421
).to.be.rejectedWith(FirebaseError);
420422
});
421423
});
424+
425+
describe("ensureAllRequiredAPIsEnabled", () => {
426+
let sinonSandbox: sinon.SinonSandbox;
427+
let ensureApiStub: sinon.SinonStub;
428+
let generateServiceIdentityStub: sinon.SinonStub;
429+
430+
beforeEach(() => {
431+
sinonSandbox = sinon.createSandbox();
432+
ensureApiStub = sinonSandbox.stub(ensureApiEnabled, "ensure").resolves();
433+
generateServiceIdentityStub = sinonSandbox
434+
.stub(serviceusage, "generateServiceIdentity")
435+
.resolves();
436+
});
437+
438+
afterEach(() => {
439+
sinonSandbox.restore();
440+
});
441+
442+
it("should not enable any APIs for an empty backend", async () => {
443+
await prepare.ensureAllRequiredAPIsEnabled("project", backend.empty());
444+
expect(ensureApiStub.called).to.be.false;
445+
expect(generateServiceIdentityStub.called).to.be.false;
446+
});
447+
448+
it("should enable APIs from backend.requiredAPIs", async () => {
449+
const api1 = "testapi1.googleapis.com";
450+
const api2 = "testapi2.googleapis.com";
451+
const b = backend.empty();
452+
b.requiredAPIs = [{ api: api1 }, { api: api2 }];
453+
454+
await prepare.ensureAllRequiredAPIsEnabled("project", b);
455+
expect(ensureApiStub.calledWith("project", api1, "functions", false)).to.be.true;
456+
expect(ensureApiStub.calledWith("project", api2, "functions", false)).to.be.true;
457+
});
458+
459+
it("should enable Secret Manager API if secrets are used ", async () => {
460+
const e: backend.Endpoint = {
461+
id: "hasSecrets",
462+
platform: "gcfv1",
463+
region: "us-central1",
464+
project: "project",
465+
entryPoint: "entry",
466+
runtime: "nodejs22",
467+
httpsTrigger: {},
468+
secretEnvironmentVariables: [
469+
{
470+
key: "SECRET",
471+
secret: "secret",
472+
projectId: "project",
473+
},
474+
],
475+
};
476+
await prepare.ensureAllRequiredAPIsEnabled("project", backend.of(e));
477+
expect(
478+
ensureApiStub.calledWith(
479+
"project",
480+
"https://secretmanager.googleapis.com",
481+
"functions",
482+
false,
483+
),
484+
).to.be.true;
485+
});
486+
487+
it("should enable GCFv2 APIs and generate required service identities", async () => {
488+
const e: backend.Endpoint = {
489+
id: "v2",
490+
platform: "gcfv2",
491+
region: "us-central1",
492+
project: "project",
493+
entryPoint: "entry",
494+
runtime: "nodejs22",
495+
httpsTrigger: {},
496+
};
497+
498+
await prepare.ensureAllRequiredAPIsEnabled("project", backend.of(e));
499+
500+
expect(ensureApiStub.calledWith("project", "https://run.googleapis.com", "functions")).to.be
501+
.true;
502+
expect(ensureApiStub.calledWith("project", "https://eventarc.googleapis.com", "functions")).to
503+
.be.true;
504+
expect(ensureApiStub.calledWith("project", "https://pubsub.googleapis.com", "functions")).to
505+
.be.true;
506+
expect(ensureApiStub.calledWith("project", "https://storage.googleapis.com", "functions")).to
507+
.be.true;
508+
expect(
509+
generateServiceIdentityStub.calledWith("project", "pubsub.googleapis.com", "functions"),
510+
).to.be.true;
511+
expect(
512+
generateServiceIdentityStub.calledWith("project", "eventarc.googleapis.com", "functions"),
513+
).to.be.true;
514+
});
515+
});
422516
});

src/deploy/functions/prepare.ts

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
eventarcOrigin,
1919
pubsubOrigin,
2020
storageOrigin,
21+
secretManagerOrigin,
2122
} from "../../api";
2223
import { Options } from "../../options";
2324
import {
@@ -205,6 +206,7 @@ export async function prepare(
205206
`preparing ${clc.bold(sourceDirName)} directory for uploading...`,
206207
);
207208
}
209+
208210
if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
209211
const packagedSource = await prepareFunctionsUpload(sourceDir, config);
210212
source.functionsSourceV2 = packagedSource?.pathToSource;
@@ -241,34 +243,9 @@ export async function prepare(
241243
const wantBackend = backend.merge(...Object.values(wantBackends));
242244
const haveBackend = backend.merge(...Object.values(haveBackends));
243245

246+
await ensureAllRequiredAPIsEnabled(projectNumber, wantBackend);
244247
await warnIfNewGenkitFunctionIsMissingSecrets(wantBackend, haveBackend, options);
245248

246-
// Enable required APIs. This may come implicitly from triggers (e.g. scheduled triggers
247-
// require cloudscheudler and, in v1, require pub/sub), or can eventually come from
248-
// explicit dependencies.
249-
await Promise.all(
250-
Object.values(wantBackend.requiredAPIs).map(({ api }) => {
251-
return ensureApiEnabled.ensure(projectId, api, "functions", /* silent=*/ false);
252-
}),
253-
);
254-
if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
255-
// Note: Some of these are premium APIs that require billing to be enabled.
256-
// We'd eventually have to add special error handling for billing APIs, but
257-
// enableCloudBuild is called above and has this special casing already.
258-
const V2_APIS = [cloudRunApiOrigin(), eventarcOrigin(), pubsubOrigin(), storageOrigin()];
259-
const enablements = V2_APIS.map((api) => {
260-
return ensureApiEnabled.ensure(context.projectId, api, "functions");
261-
});
262-
await Promise.all(enablements);
263-
// Need to manually kick off the p4sa activation of services
264-
// that we use with IAM roles assignment.
265-
const services = ["pubsub.googleapis.com", "eventarc.googleapis.com"];
266-
const generateServiceAccounts = services.map((service) => {
267-
return generateServiceIdentity(projectNumber, service, "functions");
268-
});
269-
await Promise.all(generateServiceAccounts);
270-
}
271-
272249
// ===Phase 6. Ask for user prompts for things might warrant user attentions.
273250
// We limit the scope endpoints being deployed.
274251
const matchingBackend = backend.matchingBackend(wantBackend, (endpoint) => {
@@ -533,3 +510,48 @@ export async function warnIfNewGenkitFunctionIsMissingSecrets(
533510
}
534511
}
535512
}
513+
514+
// Enable required APIs. This may come implicitly from triggers (e.g. scheduled triggers
515+
// require cloudscheduler and, in v1, require pub/sub), use of features (secrets), or explicit dependencies.
516+
export async function ensureAllRequiredAPIsEnabled(
517+
projectNumber: string,
518+
wantBackend: backend.Backend,
519+
): Promise<void> {
520+
await Promise.all(
521+
Object.values(wantBackend.requiredAPIs).map(({ api }) => {
522+
return ensureApiEnabled.ensure(projectNumber, api, "functions", /* silent=*/ false);
523+
}),
524+
);
525+
if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
526+
// Note: Some of these are premium APIs that require billing to be enabled.
527+
// We'd eventually have to add special error handling for billing APIs, but
528+
// enableCloudBuild is called above and has this special casing already.
529+
const V2_APIS = [cloudRunApiOrigin(), eventarcOrigin(), pubsubOrigin(), storageOrigin()];
530+
const enablements = V2_APIS.map((api) => {
531+
return ensureApiEnabled.ensure(projectNumber, api, "functions");
532+
});
533+
await Promise.all(enablements);
534+
// Need to manually kick off the p4sa activation of services
535+
// that we use with IAM roles assignment.
536+
const services = ["pubsub.googleapis.com", "eventarc.googleapis.com"];
537+
const generateServiceAccounts = services.map((service) => {
538+
return generateServiceIdentity(projectNumber, service, "functions");
539+
});
540+
await Promise.all(generateServiceAccounts);
541+
}
542+
543+
// If function is making use of secrets, go ahead and enable Secret Manager API.
544+
if (
545+
backend.someEndpoint(
546+
wantBackend,
547+
(e) => !!(e.secretEnvironmentVariables && e.secretEnvironmentVariables.length > 0),
548+
)
549+
) {
550+
await ensureApiEnabled.ensure(
551+
projectNumber,
552+
secretManagerOrigin(),
553+
"functions",
554+
/* silent=*/ false,
555+
);
556+
}
557+
}

0 commit comments

Comments
 (0)