Skip to content

Commit db7dd17

Browse files
authoredApr 1, 2024··
Add FAH config library (#6941)
* Chose service accounts dialog * upsert secret * Config * Run formatter * Fix field rename * Formatter * run formatter * Fix refactoring bug * Docs comments
1 parent 25673ea commit db7dd17

File tree

4 files changed

+138
-0
lines changed

4 files changed

+138
-0
lines changed
 

‎src/apphosting/config.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as path from "path";
2+
import { writeFileSync } from "fs";
3+
import * as yaml from "js-yaml";
4+
5+
import * as fs from "../fsutils";
6+
7+
export interface RunConfig {
8+
concurrency?: number;
9+
cpu?: number;
10+
memoryMiB?: number;
11+
minInstances?: number;
12+
maxInstances?: number;
13+
}
14+
15+
interface HasSecret {
16+
secret: string;
17+
value: never;
18+
}
19+
interface HasValue {
20+
secret: never;
21+
value: string;
22+
}
23+
24+
/** Where an environment variable can be provided. */
25+
export type Availability = "BUILD" | "RUNTIME";
26+
27+
/** Config for an environment variable. */
28+
export type Env = (HasSecret | HasValue) & {
29+
variable: string;
30+
availability?: Availability[];
31+
};
32+
33+
/** Schema for apphosting.yaml. */
34+
export interface Config {
35+
runConfig?: RunConfig;
36+
env?: Env[];
37+
}
38+
39+
/**
40+
* Finds the path of apphosting.yaml.
41+
* Starts with cwd and walks up the tree until apphosting.yaml is found or
42+
* we find the project root (where firebase.json is) or the filesystem root;
43+
* in these cases, returns null.
44+
*/
45+
export function yamlPath(cwd: string): string | null {
46+
let dir = cwd;
47+
48+
while (!fs.fileExistsSync(path.resolve(dir, "apphosting.yaml"))) {
49+
// We've hit project root
50+
if (fs.fileExistsSync(path.resolve(dir, "firebase.json"))) {
51+
return null;
52+
}
53+
54+
const parent = path.dirname(dir);
55+
// We've hit the filesystem root
56+
if (parent === dir) {
57+
return null;
58+
}
59+
dir = parent;
60+
}
61+
return path.resolve(dir, "apphosting.yaml");
62+
}
63+
64+
/** Load apphosting.yaml */
65+
export function load(yamlPath: string): Config {
66+
const raw = fs.readFile(yamlPath);
67+
return yaml.load(raw, yaml.DEFAULT_FULL_SCHEMA) as Config;
68+
}
69+
70+
/** Save apphosting.yaml */
71+
export function store(yamlPath: string, config: Config): void {
72+
writeFileSync(yamlPath, yaml.dump(config));
73+
}

‎src/gcp/iam.ts

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ import { Client } from "../apiv2";
44

55
const apiClient = new Client({ urlPrefix: iamOrigin(), apiVersion: "v1" });
66

7+
/** Returns the default cloud build service agent */
8+
export function getDefaultCloudBuildServiceAgent(projectNumber: string): string {
9+
return `${projectNumber}@cloudbuild.gserviceaccount.com`;
10+
}
11+
12+
/** Returns the default compute engine service agent */
13+
export function getDefaultComputeEngineServiceAgent(projectNumber: string): string {
14+
return `${projectNumber}-compute@developer.gserviceaccount.com`;
15+
}
16+
717
// IAM Policy
818
// https://cloud.google.com/resource-manager/reference/rest/Shared.Types/Policy
919
export interface Binding {

‎src/init/features/apphosting/secrets.ts

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export async function grantSecretAccess(
4848
name: secretName,
4949
};
5050

51+
// TODO: Document why Cloud Build SA needs viewer permission but Run doesn't.
52+
// TODO: future proof for when therte is a single service account (currently will set the same
53+
// secretAccessor permission twice)
5154
const newBindings: iam.Binding[] = [
5255
{
5356
role: "roles/secretmanager.secretAccessor",

‎src/test/apphosting/config.spec.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
4+
import * as fsImport from "../../fsutils";
5+
import * as config from "../../apphosting/config";
6+
7+
describe("config", () => {
8+
describe("yamlPath", () => {
9+
let fs: sinon.SinonStubbedInstance<typeof fsImport>;
10+
11+
beforeEach(() => {
12+
fs = sinon.stub(fsImport);
13+
});
14+
15+
afterEach(() => {
16+
sinon.verifyAndRestore();
17+
});
18+
19+
it("finds apphosting.yaml at cwd", () => {
20+
fs.fileExistsSync.withArgs("/cwd/apphosting.yaml").returns(true);
21+
expect(config.yamlPath("/cwd")).equals("/cwd/apphosting.yaml");
22+
});
23+
24+
it("finds apphosting.yaml in a parent directory", () => {
25+
fs.fileExistsSync.withArgs("/parent/cwd/apphosting.yaml").returns(false);
26+
fs.fileExistsSync.withArgs("/parent/cwd/firebase.json").returns(false);
27+
fs.fileExistsSync.withArgs("/parent/apphosting.yaml").returns(true);
28+
29+
expect(config.yamlPath("/parent/cwd")).equals("/parent/apphosting.yaml");
30+
});
31+
32+
it("returns null if it finds firebase.json without finding apphosting.yaml", () => {
33+
fs.fileExistsSync.withArgs("/parent/cwd/apphosting.yaml").returns(false);
34+
fs.fileExistsSync.withArgs("/parent/cwd/firebase.json").returns(false);
35+
fs.fileExistsSync.withArgs("/parent/apphosting.yaml").returns(false);
36+
fs.fileExistsSync.withArgs("/parent/firebase.json").returns(true);
37+
38+
expect(config.yamlPath("/parent/cwd")).equals(null);
39+
});
40+
41+
it("returns if it reaches the fs root", () => {
42+
fs.fileExistsSync.withArgs("/parent/cwd/apphosting.yaml").returns(false);
43+
fs.fileExistsSync.withArgs("/parent/cwd/firebase.json").returns(false);
44+
fs.fileExistsSync.withArgs("/parent/apphosting.yaml").returns(false);
45+
fs.fileExistsSync.withArgs("/parent/firebase.json").returns(false);
46+
fs.fileExistsSync.withArgs("/apphosting.yaml").returns(false);
47+
fs.fileExistsSync.withArgs("/firebase.json").returns(false);
48+
49+
expect(config.yamlPath("/parent/cwd")).equals(null);
50+
});
51+
});
52+
});

0 commit comments

Comments
 (0)
Please sign in to comment.