Skip to content

feat: Add .env.<codebase> support for multi-instance functions #8967

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/deploy/functions/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,9 @@
const config = configForCodebase(context.config, codebase);
const firebaseEnvs = functionsEnv.loadFirebaseEnvs(firebaseConfig, projectId);
const userEnvOpt: functionsEnv.UserEnvsOpts = {
projectId,
codebase,
functionsSource: options.config.path(config.source),
projectId: projectId,
projectAlias: options.projectAlias,
};
const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
Expand Down Expand Up @@ -376,7 +377,7 @@
.filter(
(ep) =>
backend.isBlockingTriggered(ep) &&
AUTH_BLOCKING_EVENTS.includes(ep.blockingTrigger.eventType as any),

Check warning on line 380 in src/deploy/functions/prepare.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 380 in src/deploy/functions/prepare.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `"providers/cloud.auth/eventTypes/user.beforeCreate" | "providers/cloud.auth/eventTypes/user.beforeSignIn" | "providers/cloud.auth/eventTypes/user.beforeSendEmail" | "providers/cloud.auth/eventTypes/user.beforeSendSms"`
) as (backend.Endpoint & backend.BlockingTriggered)[];

if (authBlockingEndpoints.length === 0) {
Expand Down Expand Up @@ -490,7 +491,7 @@
// Genkit almost always requires an API key, so warn if the customer is about to deploy
// a function and doesn't have one. To avoid repetitive nagging, only warn on the first
// deploy of the function.
export async function warnIfNewGenkitFunctionIsMissingSecrets(

Check warning on line 494 in src/deploy/functions/prepare.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment

Check warning on line 494 in src/deploy/functions/prepare.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
have: backend.Backend,
want: backend.Backend,
options: DeployOptions,
Expand Down Expand Up @@ -524,7 +525,7 @@

// Enable required APIs. This may come implicitly from triggers (e.g. scheduled triggers
// require cloudscheduler and, in v1, require pub/sub), use of features (secrets), or explicit dependencies.
export async function ensureAllRequiredAPIsEnabled(

Check warning on line 528 in src/deploy/functions/prepare.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
projectNumber: string,
wantBackend: backend.Backend,
): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion src/deploy/functions/runtimes/node/parseTriggers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
storageBucket: "foo.appspot.com",
databaseURL: "https://foo.firebaseio.com",
},
userEnvOpt: { functionsSource: "", projectId: "PROJECT" },
userEnvOpt: { functionsSource: "", projectId: "PROJECT", codebase: "default" },
userEnvs: {},
})
).backend;
Expand All @@ -26,13 +26,13 @@
describe("addResourcesToBuild", () => {
const oldDefaultRegion = api.functionsDefaultRegion();
before(() => {
(api as any).functionsDefaultRegion = () => {

Check warning on line 29 in src/deploy/functions/runtimes/node/parseTriggers.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 29 in src/deploy/functions/runtimes/node/parseTriggers.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .functionsDefaultRegion on an `any` value
return "us-central1";
};
});

after(() => {
(api as any).functionsDefaultRegion = () => {

Check warning on line 35 in src/deploy/functions/runtimes/node/parseTriggers.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 35 in src/deploy/functions/runtimes/node/parseTriggers.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .functionsDefaultRegion on an `any` value
return oldDefaultRegion;
};
});
Expand Down Expand Up @@ -491,7 +491,7 @@
describe("addResourcesToBackend", () => {
const oldDefaultRegion = api.functionsDefaultRegion();
before(() => {
(api as any).functionsDefaultRegion = () => {

Check warning on line 494 in src/deploy/functions/runtimes/node/parseTriggers.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .functionsDefaultRegion on an `any` value
return "us-central1";
};
});
Expand Down
2 changes: 2 additions & 0 deletions src/emulator/functionsEmulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@ export class FunctionsEmulator implements EmulatorInstance {
projectId: this.args.projectId,
projectAlias: this.args.projectAlias,
isEmulator: true,
codebase: emulatableBackend.codebase,
};
const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
const discoveredBuild = await runtimeDelegate.discoverBuild(runtimeConfig, environment);
Expand Down Expand Up @@ -1378,6 +1379,7 @@ export class FunctionsEmulator implements EmulatorInstance {
projectId: this.args.projectId,
projectAlias: this.args.projectAlias,
isEmulator: true,
codebase: backend.codebase,
};

if (functionsEnv.hasUserEnvs(projectInfo)) {
Expand Down
200 changes: 175 additions & 25 deletions src/functions/env.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,19 +310,33 @@ FOO=foo
it("never affects the filesystem if the list of keys to write is empty", () => {
env.writeUserEnvs(
{},
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
{
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
},
);
env.writeUserEnvs(
{},
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, isEmulator: true },
{
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
isEmulator: true,
codebase: "default",
},
);
expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).to.throw;
expect(() => fs.statSync(path.join(tmpdir, ".env.project"))).to.throw;
expect(() => fs.statSync(path.join(tmpdir, ".env.local"))).to.throw;
});

it("touches .env.projectId if it doesn't already exist", () => {
env.writeUserEnvs({ FOO: "bar" }, { projectId: "project", functionsSource: tmpdir });
env.writeUserEnvs(
{ FOO: "bar" },
{ projectId: "project", functionsSource: tmpdir, codebase: "default" },
);
expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).to.throw;
expect(!!fs.statSync(path.join(tmpdir, ".env.project"))).to.be.true;
expect(() => fs.statSync(path.join(tmpdir, ".env.local"))).to.throw;
Expand All @@ -331,7 +345,7 @@ FOO=foo
it("touches .env.local if it doesn't already exist in emulator mode", () => {
env.writeUserEnvs(
{ FOO: "bar" },
{ projectId: "project", functionsSource: tmpdir, isEmulator: true },
{ projectId: "project", functionsSource: tmpdir, isEmulator: true, codebase: "default" },
);
expect(() => fs.statSync(path.join(tmpdir, ".env.alias"))).to.throw;
expect(() => fs.statSync(path.join(tmpdir, ".env.project"))).to.throw;
Expand All @@ -343,7 +357,10 @@ FOO=foo
[".env.project"]: "FOO=foo",
});
expect(() =>
env.writeUserEnvs({ FOO: "bar" }, { projectId: "project", functionsSource: tmpdir }),
env.writeUserEnvs(
{ FOO: "bar" },
{ projectId: "project", functionsSource: tmpdir, codebase: "default" },
),
).to.throw(FirebaseError);
});

Expand All @@ -353,14 +370,15 @@ FOO=foo
});
env.writeUserEnvs(
{ FOO: "bar" },
{ projectId: "project", functionsSource: tmpdir, isEmulator: true },
{ projectId: "project", functionsSource: tmpdir, isEmulator: true, codebase: "default" },
);
expect(
env.loadUserEnvs({
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
isEmulator: true,
codebase: "default",
})["FOO"],
).to.equal("bar");
});
Expand All @@ -372,7 +390,12 @@ FOO=foo
expect(() =>
env.writeUserEnvs(
{ FOO: "baz" },
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
{
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
},
),
).to.throw(FirebaseError);
});
Expand All @@ -383,14 +406,21 @@ FOO=foo
});
env.writeUserEnvs(
{ FOO: "baz" },
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir, isEmulator: true },
{
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
isEmulator: true,
codebase: "default",
},
);
expect(
env.loadUserEnvs({
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
isEmulator: true,
codebase: "default",
})["FOO"],
).to.equal("baz");
});
Expand All @@ -402,7 +432,7 @@ FOO=foo
expect(() =>
env.writeUserEnvs(
{ ASDF: "bar" },
{ projectId: "project", functionsSource: tmpdir, isEmulator: true },
{ projectId: "project", functionsSource: tmpdir, isEmulator: true, codebase: "default" },
),
).to.throw(FirebaseError);
});
Expand All @@ -411,32 +441,55 @@ FOO=foo
expect(() =>
env.writeUserEnvs(
{ lowercase: "bar" },
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
{
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
},
),
).to.throw(env.KeyValidationError);
expect(() =>
env.writeUserEnvs(
{ GCP_PROJECT: "bar" },
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
{
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
},
),
).to.throw(env.KeyValidationError);
expect(() =>
env.writeUserEnvs(
{ FIREBASE_KEY: "bar" },
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
{
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
},
),
).to.throw(env.KeyValidationError);
});

it("writes the specified key to a .env.projectId that it created", () => {
env.writeUserEnvs(
{ FOO: "bar" },
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
{
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
},
);
expect(
env.loadUserEnvs({ projectId: "project", projectAlias: "alias", functionsSource: tmpdir })[
"FOO"
],
env.loadUserEnvs({
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
})["FOO"],
).to.equal("bar");
});

Expand All @@ -446,24 +499,38 @@ FOO=foo
});
env.writeUserEnvs(
{ FOO: "bar" },
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
{
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
},
);
expect(
env.loadUserEnvs({ projectId: "project", projectAlias: "alias", functionsSource: tmpdir })[
"FOO"
],
env.loadUserEnvs({
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
})["FOO"],
).to.equal("bar");
});

it("writes multiple keys at once", () => {
env.writeUserEnvs(
{ FOO: "foo", BAR: "bar" },
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
{
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
},
);
const envs = env.loadUserEnvs({
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
});
expect(envs["FOO"]).to.equal("foo");
expect(envs["BAR"]).to.equal("bar");
Expand All @@ -476,12 +543,18 @@ FOO=foo
WITH_SLASHES: "\n\\\r\\\t\\\v",
QUOTES: "'\"'",
},
{ projectId: "project", projectAlias: "alias", functionsSource: tmpdir },
{
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
},
);
const envs = env.loadUserEnvs({
projectId: "project",
projectAlias: "alias",
functionsSource: tmpdir,
codebase: "default",
});
expect(envs["ESCAPES"]).to.equal("\n\r\t\v");
expect(envs["WITH_SLASHES"]).to.equal("\n\\\r\\\t\\\v");
Expand All @@ -492,13 +565,16 @@ FOO=foo
try {
env.writeUserEnvs(
{ FOO: "bar", lowercase: "bar" },
{ projectId: "project", functionsSource: tmpdir },
{ projectId: "project", functionsSource: tmpdir, codebase: "default" },
);
} catch (err: any) {
// no-op
}
expect(env.loadUserEnvs({ projectId: "project", functionsSource: tmpdir })["FOO"]).to.be
.undefined;
expect(
env.loadUserEnvs({ projectId: "project", functionsSource: tmpdir, codebase: "default" })[
"FOO"
],
).to.be.undefined;
});
});

Expand All @@ -511,6 +587,7 @@ FOO=foo
const projectInfo: Omit<env.UserEnvsOpts, "functionsSource"> = {
projectId: "my-project",
projectAlias: "dev",
codebase: "default",
};
let tmpdir: string;

Expand Down Expand Up @@ -695,6 +772,79 @@ FOO=foo
env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir });
}).to.throw("Failed to load");
});

describe("codebase environment variables", () => {
it("loads envs from .env.<codebase> file", () => {
createEnvFiles(tmpdir, {
[`.env.${projectInfo.codebase}`]: "FOO=codebase-foo\nBAR=codebase-bar",
});

expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({
FOO: "codebase-foo",
BAR: "codebase-bar",
});
});

it("loads envs with correct precedence: .env < .env.<codebase> < .env.<project>", () => {
createEnvFiles(tmpdir, {
".env": "FOO=global\nBAR=global\nBAZ=global",
[`.env.${projectInfo.codebase}`]: "FOO=codebase\nBAR=codebase",
[`.env.${projectInfo.projectId}`]: "FOO=project",
});

expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({
FOO: "project", // project overrides codebase
BAR: "codebase", // codebase overrides global
BAZ: "global", // only defined in global
});
});

it("loads envs with correct precedence: .env < .env.<codebase> < .env.<alias>", () => {
createEnvFiles(tmpdir, {
".env": "FOO=global\nBAR=global\nBAZ=global",
[`.env.${projectInfo.codebase}`]: "FOO=codebase\nBAR=codebase",
[`.env.${projectInfo.projectAlias}`]: "FOO=alias",
});

expect(env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir })).to.be.deep.equal({
FOO: "alias", // alias overrides codebase
BAR: "codebase", // codebase overrides global
BAZ: "global", // only defined in global
});
});

it("works with custom codebase names", () => {
const customProjectInfo = { ...projectInfo, codebase: "profile-pics-resizer" };
createEnvFiles(tmpdir, {
".env": "FOO=global",
".env.profile-pics-resizer": "FOO=custom-codebase\nCUSTOM=value",
});

expect(
env.loadUserEnvs({ ...customProjectInfo, functionsSource: tmpdir }),
).to.be.deep.equal({
FOO: "custom-codebase",
CUSTOM: "value",
});
});

it("loads envs correctly for emulator with .env.local precedence", () => {
createEnvFiles(tmpdir, {
".env": "FOO=global\nBAR=global\nBAZ=global",
[`.env.${projectInfo.codebase}`]: "FOO=codebase\nBAR=codebase",
[`.env.${projectInfo.projectId}`]: "FOO=project",
".env.local": "FOO=local",
});

expect(
env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir, isEmulator: true }),
).to.be.deep.equal({
FOO: "local", // .env.local has highest precedence in emulator
BAR: "codebase", // codebase overrides global
BAZ: "global", // only defined in global
});
});
});
});

describe("parseStrict", () => {
Expand Down
Loading
Loading