Skip to content
17 changes: 17 additions & 0 deletions src/deploy/functions/functionsDeployHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,23 @@
return id;
}

/**
* Explains why a gcfv1 endpoint may not take over a name that already exists as something
* else, or undefined if the update is legal. A gcfv1 function is a different resource to a
* gcfv2 function or a Cloud Run service, so the CLI cannot update one into the other.
* Shared so that prepare-time validation and the release planner cannot drift apart.
*/
export function generationDowngradeMessage(
want: backend.Endpoint,
have: backend.Endpoint,
): string | undefined {
if (want.platform !== "gcfv1" || have.platform === "gcfv1") {
return undefined;
}
const from = have.platform === "gcfv2" ? "GCFv2" : "Cloud Run";
return `[${getFunctionLabel(want)}] Functions cannot be downgraded from ${from} to GCFv1`;
}

/**
* Returns list of codebases specified in firebase.json filtered by --only filters if present.
*/
Expand Down Expand Up @@ -241,6 +258,6 @@
}

/** Checks if a function should be filtered given a list of endpoints. */
export function isEndpointFiltered(endpoint: backend.Endpoint, filters: EndpointFilter[]) {

Check warning on line 261 in src/deploy/functions/functionsDeployHelper.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Missing return type on function
return filters.some((filter) => endpointMatchesFilter(endpoint, filter));
}
52 changes: 52 additions & 0 deletions src/deploy/functions/prepare.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,11 @@
.to.be.rejectedWith(FirebaseError)
.then((error) => {
// Should always list latest runtimes
expect(error.message).to.include(latest("nodejs"));

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

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .message on an `any` value
expect(error.message).to.include(latest("python"));

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

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .message on an `any` value

// Should never list a decommissioned runtime
expect(error.message).to.not.include("nodejs6");

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

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .message on an `any` value
});
});

Expand Down Expand Up @@ -879,13 +879,65 @@
...ENDPOINT_BASE,
httpsTrigger: {},
};
const have: backend.Endpoint = JSON.parse(JSON.stringify(want));

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

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe assignment of an `any` value
have.timeoutSeconds = 120;

prepare.inferDetailsFromExisting(backend.of(want), backend.of(have), /* usedDotEnv= */ false);
expect(want.timeoutSeconds).to.equal(120);
});

it("does not inherit cpu onto a gcfv1 endpoint", () => {
// Redeploying an existing gcfv2 function as gcfv1 (e.g. as a v1 blocking auth
// trigger). Inheriting cpu here fails CPU validation and masks the real
// "cannot be downgraded" error. Memory and timeout exist on both generations.
const have: backend.Endpoint = {
...ENDPOINT_BASE,
platform: "gcfv2",
httpsTrigger: {},
cpu: 1,
availableMemoryMb: 512,
timeoutSeconds: 120,
};
const want: backend.Endpoint = {
...ENDPOINT_BASE,
platform: "gcfv1",
httpsTrigger: {},
};

prepare.inferDetailsFromExisting(backend.of(want), backend.of(have), /* usedDotEnv= */ false);

expect(want.cpu).to.be.undefined;
expect(want.availableMemoryMb).to.equal(512);
expect(want.timeoutSeconds).to.equal(120);
});

for (const [havePlatform, wantPlatform] of [
["run", "gcfv2"],
["gcfv2", "run"],
] as const) {
it(`inherits cpu from ${havePlatform} onto ${wantPlatform}`, () => {
const have: backend.Endpoint = {
...ENDPOINT_BASE,
platform: havePlatform,
httpsTrigger: {},
cpu: 2,
};
const want: backend.Endpoint = {
...ENDPOINT_BASE,
platform: wantPlatform,
httpsTrigger: {},
};

prepare.inferDetailsFromExisting(
backend.of(want),
backend.of(have),
/* usedDotEnv= */ false,
);

expect(want.cpu).to.equal(2);
});
}

it("downgrades concurrency if necessary (explicit)", () => {
const have: backend.Endpoint = {
...ENDPOINT_BASE,
Expand Down Expand Up @@ -1136,7 +1188,7 @@
await prepare.warnIfNewGenkitFunctionIsMissingSecrets(
backend.empty(),
backend.of(nonGenkitEndpoint),
{} as any,

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

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

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

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe argument of type `any` assigned to a parameter of type `DeployOptions`
);
expect(confirm).to.not.be.called;
});
Expand All @@ -1145,7 +1197,7 @@
await prepare.warnIfNewGenkitFunctionIsMissingSecrets(
backend.empty(),
backend.of(genkitEndpointWithSecrets),
{} as any,

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

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

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

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe argument of type `any` assigned to a parameter of type `DeployOptions`
);
expect(confirm).to.not.be.called;
});
Expand All @@ -1154,7 +1206,7 @@
await prepare.warnIfNewGenkitFunctionIsMissingSecrets(
backend.of(genkitEndpointWithoutSecrets),
backend.of(genkitEndpointWithoutSecrets),
{} as any,

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

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe argument of type `any` assigned to a parameter of type `DeployOptions`
);
expect(confirm).to.not.be.called;
});
Expand Down
5 changes: 4 additions & 1 deletion src/deploy/functions/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,10 @@ export function inferDetailsFromExisting(
wantE.availableMemoryMb = haveE.availableMemoryMb;
}

if (typeof wantE.cpu === "undefined" && haveE.cpu) {
// cpu does not exist on gcfv1. Inheriting it from an existing gcfv2 function onto
// a gcfv1 endpoint fails CPU validation and masks the accurate "cannot be
// downgraded" error.
if (typeof wantE.cpu === "undefined" && haveE.cpu && wantE.platform !== "gcfv1") {
wantE.cpu = haveE.cpu;
}

Expand Down
8 changes: 4 additions & 4 deletions src/deploy/functions/release/planner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
EndpointFilter,
endpointMatchesAnyFilter,
generationDowngradeMessage,
getFunctionLabel,
} from "../functionsDeployHelper";
import { isFirebaseManaged } from "../../../deploymentTool";
Expand Down Expand Up @@ -382,10 +383,9 @@ export function checkForIllegalUpdate(want: backend.Endpoint, have: backend.Endp
)}] Changing from ${haveType} function to ${wantType} function is not allowed. Please delete your function and create a new one instead.`,
);
}
if (want.platform === "gcfv1" && have.platform === "gcfv2") {
throw new FirebaseError(
`[${getFunctionLabel(want)}] Functions cannot be downgraded from GCFv2 to GCFv1`,
);
const downgrade = generationDowngradeMessage(want, have);
if (downgrade) {
throw new FirebaseError(downgrade);
}

// We need to call from module exports so tests can stub this behavior, but that
Expand Down
24 changes: 24 additions & 0 deletions src/deploy/functions/validate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,30 @@ describe("validate", () => {
httpsTrigger: {},
};

it("rejects downgrading an existing gcfv2 function to gcfv1", () => {
const want = backend.of({ ...ENDPOINT_BASE, platform: "gcfv1" });
const have = backend.of({ ...ENDPOINT_BASE, platform: "gcfv2", cpu: 1 });

expect(() => validate.endpointsAreValid(want, have)).to.throw(
/cannot be downgraded from GCFv2 to GCFv1/,
);
});

it("rejects redeploying an existing Cloud Run service as gcfv1", () => {
const want = backend.of({ ...ENDPOINT_BASE, platform: "gcfv1" });
const have = backend.of({ ...ENDPOINT_BASE, platform: "run", cpu: 1 });

expect(() => validate.endpointsAreValid(want, have)).to.throw(
/cannot be downgraded from Cloud Run to GCFv1/,
);
});

it("allows a gcfv1 function that does not exist yet", () => {
const want = backend.of({ ...ENDPOINT_BASE, platform: "gcfv1" });

expect(() => validate.endpointsAreValid(want, backend.empty())).to.not.throw();
});

it("disallows concurrency for GCF gen 1", () => {
const ep: backend.Endpoint = {
...ENDPOINT_BASE,
Expand Down
27 changes: 26 additions & 1 deletion src/deploy/functions/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import * as clc from "colorette";
import { FirebaseError } from "../../error";
import { getSecretVersion, SecretVersion } from "../../gcp/secretManager";
import { logger } from "../../logger";
import { EndpointFilter, endpointMatchesFilter, getFunctionLabel } from "./functionsDeployHelper";
import {
EndpointFilter,
endpointMatchesFilter,
generationDowngradeMessage,
getFunctionLabel,
} from "./functionsDeployHelper";
import { serviceForEndpoint } from "./services";
import * as fsutils from "../../fsutils";
import * as backend from "./backend";
Expand Down Expand Up @@ -91,6 +96,9 @@ export function endpointsAreValid(
validateLifecycleHooks(wantBackend, existingBackend);
const endpoints = backend.allEndpoints(wantBackend);
functionIdsAreValid(endpoints);
if (existingBackend) {
noGenerationDowngrades(wantBackend, existingBackend);
}
validateTimeoutConfig(endpoints);
for (const ep of endpoints) {
validateScheduledTimeout(ep);
Expand Down Expand Up @@ -132,6 +140,23 @@ export function endpointsAreValid(
cpuConfigIsValid(endpoints);
}

/**
* Rejects an existing gcfv2 function or Cloud Run service being redeployed as gcfv1. The
* release planner enforces this too, but only after the source has been uploaded.
*/
function noGenerationDowngrades(
wantBackend: backend.Backend,
existingBackend: backend.Backend,
): void {
for (const want of backend.allEndpoints(wantBackend)) {
const have = existingBackend.endpoints[want.region]?.[want.id];
const msg = have && generationDowngradeMessage(want, have);
if (msg) {
throw new FirebaseError(msg);
}
}
}

/**
* Validate that endpoints have valid CPU configuration.
* Enforces https://cloud.google.com/run/docs/configuring/cpu.
Expand Down
Loading