Skip to content
Open
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
9 changes: 9 additions & 0 deletions manual_verify/bin/server.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'dart:io';
void main() async {
final port = int.parse(Platform.environment['PORT'] ?? '8080');
final server = await HttpServer.bind('0.0.0.0', port);
print('Server listening on port $port');
await for (final request in server) {
request.response..statusCode = HttpStatus.ok..write('Hello!')..close();
}
}
Binary file added manual_verify/bin/server.exe
Binary file not shown.
5 changes: 5 additions & 0 deletions manual_verify/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"functions": {
"source": "."
}
}
9 changes: 9 additions & 0 deletions manual_verify/functions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
endpoints:
darttest-verified-9:
platform: "run"
region: ["us-west1"]
maxInstances: 10
httpsTrigger: {}
baseImageUri: "us-west1-docker.pkg.dev/serverless-runtimes/google-22-full/runtimes/go123"
command: ["./bin/server.exe"]
entryPoint: "server"
2 changes: 1 addition & 1 deletion src/deploy/functions/backend.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe("Backend", () => {
},
},
],
containerConcurrency: 80,
maxInstanceRequestConcurrency: 80,
},
generation: 1,
createTime: "2023-01-01T00:00:00Z",
Expand Down
5 changes: 5 additions & 0 deletions src/deploy/functions/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@
return allMemoryOptions.includes(mem as MemoryOptions);
}

export function isValidEgressSetting(egress: unknown): egress is VpcEgressSettings {

Check warning on line 185 in src/deploy/functions/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
return egress === "PRIVATE_RANGES_ONLY" || egress === "ALL_TRAFFIC";
}

Expand Down Expand Up @@ -386,6 +386,11 @@

// State of the endpoint.
state?: EndpointState;

// Fields for "run" platform (no-build)
baseImageUri?: string;
command?: string[];
args?: string[];
};

export interface RequiredAPI {
Expand Down Expand Up @@ -551,8 +556,8 @@
existingBackend.endpoints[endpoint.region] || {};
existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
}
} catch (err: any) {

Check warning on line 559 in src/deploy/functions/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
logger.debug(err.message);

Check warning on line 560 in src/deploy/functions/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .message on an `any` value

Check warning on line 560 in src/deploy/functions/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Error`
unreachableRegions.run = ["unknown"];
}
} else {
Expand Down
14 changes: 11 additions & 3 deletions src/deploy/functions/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@
const allMemoryOptions: MemoryOption[] = [128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768];

// Run is an automatic migration from gcfv2 and is not used on the wire.
export type FunctionsPlatform = Exclude<backend.FunctionsPlatform, "run">;
export const AllFunctionsPlatforms: FunctionsPlatform[] = ["gcfv1", "gcfv2"];
export type FunctionsPlatform = backend.FunctionsPlatform;
export const AllFunctionsPlatforms: FunctionsPlatform[] = ["gcfv1", "gcfv2", "run"];
export type VpcEgressSetting = backend.VpcEgressSettings;
export const AllVpcEgressSettings: VpcEgressSetting[] = ["PRIVATE_RANGES_ONLY", "ALL_TRAFFIC"];
export type IngressSetting = backend.IngressSettings;
Expand All @@ -224,7 +224,7 @@
omit?: Field<boolean>;

// Defaults to "gcfv2". "Run" will be an additional option defined later
platform?: "gcfv1" | "gcfv2";
platform?: "gcfv1" | "gcfv2" | "run";

// Necessary for the GCF API to determine what code to load with the Functions Framework.
// Will become optional once "run" is supported as a platform
Expand Down Expand Up @@ -269,6 +269,11 @@
environmentVariables?: Record<string, string | Expression<string>> | null;
secretEnvironmentVariables?: SecretEnvVar[] | null;
labels?: Record<string, string | Expression<string>> | null;

// Fields for "run" platform (no-build)
baseImageUri?: string;
command?: string[];
args?: string[];
};

type SecretParam = ReturnType<typeof defineSecret>;
Expand Down Expand Up @@ -456,7 +461,7 @@
// List param, we try resolving a String param instead.
try {
regions = params.resolveList(bdEndpoint.region, paramValues);
} catch (err: any) {

Check warning on line 464 in src/deploy/functions/build.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
if (err instanceof ExprParseError) {
regions = [params.resolveString(bdEndpoint.region, paramValues)];
} else {
Expand Down Expand Up @@ -485,6 +490,9 @@
"environmentVariables",
"labels",
"secretEnvironmentVariables",
"baseImageUri",
"command",
"args",
);
r.resolveStrings(bkEndpoint, bdEndpoint, "serviceAccount");

Expand Down
4 changes: 2 additions & 2 deletions src/deploy/functions/checkIam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
["iam.serviceAccounts.actAs"],
);
passed = iamResult.passed;
} catch (err: any) {

Check warning on line 44 in src/deploy/functions/checkIam.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
logger.debug("[functions] service account IAM check errored, deploy may fail:", err);
// we want to fail this check open and not rethrow since it's informational only
return;
Expand All @@ -61,7 +61,6 @@
/**
* Checks a functions deployment for HTTP function creation, and tests IAM
* permissions accordingly.
*
* @param context The deploy context.
* @param options The command-wide options object.
* @param payload The deploy payload.
Expand All @@ -74,11 +73,12 @@
if (!payload.functions) {
return;
}
const filters = context.filters || getEndpointFilters(options, context.config!);

Check warning on line 76 in src/deploy/functions/checkIam.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
const wantBackends = Object.values(payload.functions).map(({ wantBackend }) => wantBackend);
const httpEndpoints = [...flattenArray(wantBackends.map((b) => backend.allEndpoints(b)))]
.filter(backend.isHttpsTriggered)
.filter((f) => endpointMatchesAnyFilter(f, filters));
.filter((f) => endpointMatchesAnyFilter(f, filters))
.filter((f) => f.platform !== "run");

const existing = await backend.existingBackend(context);
const newHttpsEndpoints = httpEndpoints.filter(backend.missingEndpoint(existing));
Expand All @@ -97,7 +97,7 @@
try {
const iamResult = await iam.testIamPermissions(context.projectId, [PERMISSION]);
passed = iamResult.passed;
} catch (e: any) {

Check warning on line 100 in src/deploy/functions/checkIam.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
logger.debug(
"[functions] failed http create setIamPolicy permission check. deploy may fail:",
e,
Expand Down Expand Up @@ -130,7 +130,7 @@
}

/** Callback reducer function */
function reduceEventsToServices(services: Array<Service>, endpoint: backend.Endpoint) {

Check warning on line 133 in src/deploy/functions/checkIam.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
const service = serviceForEndpoint(endpoint);
if (service.requiredProjectBindings && !services.find((s) => s.name === service.name)) {
services.push(service);
Expand All @@ -149,7 +149,7 @@
* Finds the required project level IAM bindings for the Pub/Sub service agent.
* If the user enabled Pub/Sub on or before April 8, 2021, then we must enable the token creator role.
* @param projectNumber project number
* @param existingPolicy the project level IAM policy

Check warning on line 152 in src/deploy/functions/checkIam.ts

View workflow job for this annotation

GitHub Actions / lint (20)

@param "existingPolicy" does not match an existing function parameter
*/
export function obtainPubSubServiceAgentBindings(projectNumber: string): iam.Binding[] {
const serviceAccountTokenCreatorBinding: iam.Binding = {
Expand Down
13 changes: 11 additions & 2 deletions src/deploy/functions/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import * as experiments from "../../experiments";
import { findEndpoint } from "./backend";
import { deploy as extDeploy } from "../extensions";
import { getProjectNumber } from "../../getProjectNumber";
import * as path from "path";

setGracefulCleanup();

Expand Down Expand Up @@ -51,10 +52,16 @@ async function uploadSourceV1(
}

// Trampoline to allow tests to mock out createStream.
/**
*
*/
export function createReadStream(filePath: string): NodeJS.ReadableStream {
return fs.createReadStream(filePath);
}

/**
*
*/
export async function uploadSourceV2(
projectId: string,
projectNumber: string,
Expand All @@ -80,7 +87,9 @@ export async function uploadSourceV2(
};

// Legacy behavior: use the GCF API
if (!experiments.isEnabled("runfunctions")) {
// We use BYO bucket if the "runfunctions" experiment is enabled OR if we have any platform: run endpoints.
// Otherwise, we use the GCF API.
if (!experiments.isEnabled("runfunctions") && !v2Endpoints.some((e) => e.platform === "run")) {
if (process.env.GOOGLE_CLOUD_QUOTA_PROJECT) {
logLabeledWarning(
"functions",
Expand Down Expand Up @@ -116,7 +125,7 @@ export async function uploadSourceV2(
},
},
});
const objectPath = `${source.functionsSourceV2Hash}.zip`;
const objectPath = `${source.functionsSourceV2Hash}${path.extname(source.functionsSourceV2!)}`;
await gcs.upload(
uploadOpts,
`${bucketName}/${objectPath}`,
Expand Down
12 changes: 9 additions & 3 deletions src/deploy/functions/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export async function prepare(
let runtimeConfig: Record<string, unknown> = { firebase: firebaseConfig };
const allowFunctionsConfig = experiments.isEnabled("legacyRuntimeConfigCommands");

const targetedCodebaseConfigs = context.config!.filter((cfg) => codebases.includes(cfg.codebase));
const targetedCodebaseConfigs = context.config.filter((cfg) => codebases.includes(cfg.codebase));

// Load runtime config if API is enabled and at least one targeted codebase uses it
if (
Expand Down Expand Up @@ -229,8 +229,14 @@ export async function prepare(
);
}

if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
const packagedSource = await prepareFunctionsUpload(sourceDir, localCfg);
if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2" || e.platform === "run")) {
const configForUpload = shouldUseRuntimeConfig(localCfg) ? runtimeConfig : undefined;
const exportType = backend.someEndpoint(wantBackend, (e) => e.platform === "run")
? "tar.gz"
: "zip";
const packagedSource = await prepareFunctionsUpload(sourceDir, localCfg, configForUpload, {
exportType,
});
source.functionsSourceV2 = packagedSource?.pathToSource;
source.functionsSourceV2Hash = packagedSource?.hash;
}
Expand Down
19 changes: 16 additions & 3 deletions src/deploy/functions/prepareFunctionsUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ interface PackagedSourceInfo {
type SortedConfig = string | { key: string; value: SortedConfig }[];

// TODO(inlined): move to a file that's not about uploading source code
/**
*
*/
export async function getFunctionsConfig(projectId: string): Promise<Record<string, unknown>> {
try {
return await functionsConfig.materializeAll(projectId);
Expand Down Expand Up @@ -59,13 +62,16 @@ async function packageSource(
sourceDir: string,
config: projectConfig.ValidatedSingle,
runtimeConfig: any,
options?: { exportType: "zip" | "tar.gz" },
): Promise<PackagedSourceInfo | undefined> {
const tmpFile = tmp.fileSync({ prefix: "firebase-functions-", postfix: ".zip" }).name;
const exportType = options?.exportType || "zip";
const postfix = exportType === "tar.gz" ? ".tar.gz" : ".zip";
const tmpFile = tmp.fileSync({ prefix: "firebase-functions-", postfix }).name;
const fileStream = fs.createWriteStream(tmpFile, {
flags: "w",
encoding: "binary",
});
const archive = archiver("zip");
const archive = exportType === "tar.gz" ? archiver("tar", { gzip: true }) : archiver("zip");
const hashes: string[] = [];

// We must ignore firebase-debug.log or weird things happen if
Expand Down Expand Up @@ -129,14 +135,21 @@ async function packageSource(
return { pathToSource: tmpFile, hash };
}

/**
*
*/
export async function prepareFunctionsUpload(
sourceDir: string,
config: projectConfig.ValidatedSingle,
runtimeConfig?: backend.RuntimeConfigValues,
options?: { exportType: "zip" | "tar.gz" },
): Promise<PackagedSourceInfo | undefined> {
return packageSource(sourceDir, config, runtimeConfig);
return packageSource(sourceDir, config, runtimeConfig, options);
}

/**
*
*/
export function convertToSortedKeyValueArray(config: any): SortedConfig {
if (typeof config !== "object" || config === null) return config;

Expand Down
106 changes: 106 additions & 0 deletions src/deploy/functions/release/fabricator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as pollerNS from "../../../operation-poller";
import * as pubsubNS from "../../../gcp/pubsub";
import * as schedulerNS from "../../../gcp/cloudscheduler";
import * as runNS from "../../../gcp/run";
import * as runV2NS from "../../../gcp/runv2";
import * as cloudtasksNS from "../../../gcp/cloudtasks";
import * as backend from "../backend";
import * as scraper from "./sourceTokenScraper";
Expand All @@ -32,6 +33,7 @@ describe("Fabricator", () => {
let pubsub: sinon.SinonStubbedInstance<typeof pubsubNS>;
let scheduler: sinon.SinonStubbedInstance<typeof schedulerNS>;
let run: sinon.SinonStubbedInstance<typeof runNS>;
let runv2: sinon.SinonStubbedInstance<typeof runV2NS>;
let tasks: sinon.SinonStubbedInstance<typeof cloudtasksNS>;
let services: sinon.SinonStubbedInstance<typeof servicesNS>;
let identityPlatform: sinon.SinonStubbedInstance<typeof identityPlatformNS>;
Expand All @@ -44,6 +46,7 @@ describe("Fabricator", () => {
pubsub = sinon.stub(pubsubNS);
scheduler = sinon.stub(schedulerNS);
run = sinon.stub(runNS);
runv2 = sinon.stub(runV2NS);
tasks = sinon.stub(cloudtasksNS);
services = sinon.stub(servicesNS);
identityPlatform = sinon.stub(identityPlatformNS);
Expand Down Expand Up @@ -74,6 +77,10 @@ describe("Fabricator", () => {
run.setInvokerUpdate.rejects(new Error("unexpected run.setInvokerUpdate"));
run.replaceService.rejects(new Error("unexpected run.replaceService"));
run.updateService.rejects(new Error("Unexpected run.updateService"));
runv2.createService.rejects(new Error("unexpected runv2.createService"));
runv2.updateService.rejects(new Error("unexpected runv2.updateService"));
runv2.deleteService.rejects(new Error("unexpected runv2.deleteService"));
runv2.getService.rejects(new Error("unexpected runv2.getService"));
poller.pollOperation.rejects(new Error("unexpected poller.pollOperation"));
pubsub.createTopic.rejects(new Error("unexpected pubsub.createTopic"));
pubsub.deleteTopic.rejects(new Error("unexpected pubsub.deleteTopic"));
Expand Down Expand Up @@ -1637,4 +1644,103 @@ describe("Fabricator", () => {
expect(ep2Result?.error?.message).to.match(/delete function/);
});
});

describe("createRunService", () => {
it("creates a Cloud Run service with correct configuration", async () => {
runv2.createService.resolves({ uri: "https://service", name: "service" } as any);
run.setInvokerUpdate.resolves();

const ep = endpoint(
{ httpsTrigger: {} },
{
platform: "run",
baseImageUri: "gcr.io/base",
command: ["cmd"],
args: ["arg"],
},
);
await fab.createRunService(ep);

expect(runv2.createService).to.have.been.calledWith(
ep.project,
ep.region,
ep.id,
sinon.match({
template: {
containers: [
sinon.match({
image: "scratch",
baseImageUri: "gcr.io/base",
command: ["cmd"],
args: ["arg"],
sourceCode: {
cloudStorageSource: {
bucket: "bucket",
object: "object",
generation: "42",
},
},
}),
],
},
}),
);
});
});

describe("updateRunService", () => {
it("updates a Cloud Run service with correct configuration", async () => {
runv2.updateService.resolves({ uri: "https://service", name: "service" } as any);
run.setInvokerUpdate.resolves();

const ep = endpoint(
{ httpsTrigger: {} },
{
platform: "run",
baseImageUri: "gcr.io/base-v2",
},
);
// Mock update to include the endpoint
const update: planner.EndpointUpdate = {
endpoint: ep,
};

await fab.updateRunService(update);

expect(runv2.updateService).to.have.been.calledWith(
sinon.match({
name: `projects/${ep.project}/locations/${ep.region}/services/${ep.id}`,
template: {
containers: [
sinon.match({
baseImageUri: "gcr.io/base-v2",
}),
],
},
}),
);
});
});

describe("deleteRunService", () => {
it("deletes the Cloud Run service", async () => {
runv2.deleteService.resolves();
const ep = endpoint({ httpsTrigger: {} }, { platform: "run" });

await fab.deleteRunService(ep);

expect(runv2.deleteService).to.have.been.calledWith(ep.project, ep.region, ep.id);
});

it("ignores 404s", async () => {
const err = new Error("Not Found");
(err as any).status = 404;
runv2.deleteService.rejects(err);
const ep = endpoint({ httpsTrigger: {} }, { platform: "run" });

await fab.deleteRunService(ep);

expect(runv2.deleteService).to.have.been.called;
});
});
});
Loading
Loading