Skip to content

Commit

Permalink
Add FAH config library (#6941)
Browse files Browse the repository at this point in the history
* Chose service accounts dialog

* upsert secret

* Config

* Run formatter

* Fix field rename

* Formatter

* run formatter

* Fix refactoring bug

* Docs comments
  • Loading branch information
inlined authored Apr 1, 2024
1 parent 25673ea commit db7dd17
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 0 deletions.
73 changes: 73 additions & 0 deletions src/apphosting/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as path from "path";
import { writeFileSync } from "fs";
import * as yaml from "js-yaml";

import * as fs from "../fsutils";

export interface RunConfig {
concurrency?: number;
cpu?: number;
memoryMiB?: number;
minInstances?: number;
maxInstances?: number;
}

interface HasSecret {
secret: string;
value: never;
}
interface HasValue {
secret: never;
value: string;
}

/** Where an environment variable can be provided. */
export type Availability = "BUILD" | "RUNTIME";

/** Config for an environment variable. */
export type Env = (HasSecret | HasValue) & {
variable: string;
availability?: Availability[];
};

/** Schema for apphosting.yaml. */
export interface Config {
runConfig?: RunConfig;
env?: Env[];
}

/**
* Finds the path of apphosting.yaml.
* Starts with cwd and walks up the tree until apphosting.yaml is found or
* we find the project root (where firebase.json is) or the filesystem root;
* in these cases, returns null.
*/
export function yamlPath(cwd: string): string | null {
let dir = cwd;

while (!fs.fileExistsSync(path.resolve(dir, "apphosting.yaml"))) {
// We've hit project root
if (fs.fileExistsSync(path.resolve(dir, "firebase.json"))) {
return null;
}

const parent = path.dirname(dir);
// We've hit the filesystem root
if (parent === dir) {
return null;
}
dir = parent;
}
return path.resolve(dir, "apphosting.yaml");
}

/** Load apphosting.yaml */
export function load(yamlPath: string): Config {
const raw = fs.readFile(yamlPath);
return yaml.load(raw, yaml.DEFAULT_FULL_SCHEMA) as Config;
}

/** Save apphosting.yaml */
export function store(yamlPath: string, config: Config): void {
writeFileSync(yamlPath, yaml.dump(config));
}
10 changes: 10 additions & 0 deletions src/gcp/iam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import { Client } from "../apiv2";

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

/** Returns the default cloud build service agent */
export function getDefaultCloudBuildServiceAgent(projectNumber: string): string {
return `${projectNumber}@cloudbuild.gserviceaccount.com`;
}

/** Returns the default compute engine service agent */
export function getDefaultComputeEngineServiceAgent(projectNumber: string): string {
return `${projectNumber}-compute@developer.gserviceaccount.com`;
}

// IAM Policy
// https://cloud.google.com/resource-manager/reference/rest/Shared.Types/Policy
export interface Binding {
Expand Down
3 changes: 3 additions & 0 deletions src/init/features/apphosting/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export async function grantSecretAccess(
name: secretName,
};

// TODO: Document why Cloud Build SA needs viewer permission but Run doesn't.
// TODO: future proof for when therte is a single service account (currently will set the same
// secretAccessor permission twice)
const newBindings: iam.Binding[] = [
{
role: "roles/secretmanager.secretAccessor",
Expand Down
52 changes: 52 additions & 0 deletions src/test/apphosting/config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { expect } from "chai";
import * as sinon from "sinon";

import * as fsImport from "../../fsutils";
import * as config from "../../apphosting/config";

describe("config", () => {
describe("yamlPath", () => {
let fs: sinon.SinonStubbedInstance<typeof fsImport>;

beforeEach(() => {
fs = sinon.stub(fsImport);
});

afterEach(() => {
sinon.verifyAndRestore();
});

it("finds apphosting.yaml at cwd", () => {
fs.fileExistsSync.withArgs("/cwd/apphosting.yaml").returns(true);
expect(config.yamlPath("/cwd")).equals("/cwd/apphosting.yaml");
});

it("finds apphosting.yaml in a parent directory", () => {
fs.fileExistsSync.withArgs("/parent/cwd/apphosting.yaml").returns(false);
fs.fileExistsSync.withArgs("/parent/cwd/firebase.json").returns(false);
fs.fileExistsSync.withArgs("/parent/apphosting.yaml").returns(true);

expect(config.yamlPath("/parent/cwd")).equals("/parent/apphosting.yaml");
});

it("returns null if it finds firebase.json without finding apphosting.yaml", () => {
fs.fileExistsSync.withArgs("/parent/cwd/apphosting.yaml").returns(false);
fs.fileExistsSync.withArgs("/parent/cwd/firebase.json").returns(false);
fs.fileExistsSync.withArgs("/parent/apphosting.yaml").returns(false);
fs.fileExistsSync.withArgs("/parent/firebase.json").returns(true);

expect(config.yamlPath("/parent/cwd")).equals(null);
});

it("returns if it reaches the fs root", () => {
fs.fileExistsSync.withArgs("/parent/cwd/apphosting.yaml").returns(false);
fs.fileExistsSync.withArgs("/parent/cwd/firebase.json").returns(false);
fs.fileExistsSync.withArgs("/parent/apphosting.yaml").returns(false);
fs.fileExistsSync.withArgs("/parent/firebase.json").returns(false);
fs.fileExistsSync.withArgs("/apphosting.yaml").returns(false);
fs.fileExistsSync.withArgs("/firebase.json").returns(false);

expect(config.yamlPath("/parent/cwd")).equals(null);
});
});
});

0 comments on commit db7dd17

Please sign in to comment.