diff --git a/src/gcp/runv2.spec.ts b/src/gcp/runv2.spec.ts index c35e4b44efc..4ea71fb0915 100644 --- a/src/gcp/runv2.spec.ts +++ b/src/gcp/runv2.spec.ts @@ -470,6 +470,87 @@ describe("runv2", () => { expect(runv2.endpointFromService(service)).to.deep.equal(expectedEndpoint); }); + + it("should normalize ID for GCF v2 services to use dashes", () => { + const service: Omit = { + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${SERVICE_ID}`, + labels: { + [runv2.RUNTIME_LABEL]: latest("nodejs"), + [runv2.CLIENT_NAME_LABEL]: "cloud-functions", + }, + annotations: { + [runv2.FUNCTION_ID_ANNOTATION]: "v2.helloWorldNode", + }, + template: { + containers: [ + { + name: "worker", + image: IMAGE_URI, + resources: { limits: { cpu: "1", memory: "256Mi" } }, + }, + ], + }, + }; + + const result = runv2.endpointFromService(service); + expect(result.platform).to.equal("gcfv2"); + expect(result.id).to.equal("v2-helloWorldNode"); + }); + + it("should NOT normalize underscores in ID for GCF v2 services", () => { + const service: Omit = { + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${SERVICE_ID}`, + labels: { + [runv2.RUNTIME_LABEL]: latest("nodejs"), + [runv2.CLIENT_NAME_LABEL]: "cloud-functions", + }, + annotations: { + [runv2.FUNCTION_ID_ANNOTATION]: "v2_helloWorldNode", + }, + template: { + containers: [ + { + name: "worker", + image: IMAGE_URI, + resources: { limits: { cpu: "1", memory: "256Mi" } }, + }, + ], + }, + }; + + const result = runv2.endpointFromService(service); + expect(result.platform).to.equal("gcfv2"); + expect(result.id).to.equal("v2_helloWorldNode"); + }); + + it("should NOT normalize ID for non-GCF services", () => { + const service: Omit = { + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${SERVICE_ID}`, + labels: { + [runv2.RUNTIME_LABEL]: latest("nodejs"), + [runv2.CLIENT_NAME_LABEL]: "firebase-functions", + }, + annotations: { + [runv2.FUNCTION_ID_ANNOTATION]: "v2.helloWorldNode", + }, + template: { + containers: [ + { + name: "worker", + image: IMAGE_URI, + resources: { limits: { cpu: "1", memory: "256Mi" } }, + }, + ], + }, + }; + + const result = runv2.endpointFromService(service); + expect(result.platform).to.equal("run"); + expect(result.id).to.equal("v2.helloWorldNode"); + }); }); describe("listServices", () => { diff --git a/src/gcp/runv2.ts b/src/gcp/runv2.ts index 5b3a2117269..6e3becfc46f 100644 --- a/src/gcp/runv2.ts +++ b/src/gcp/runv2.ts @@ -585,23 +585,33 @@ export function endpointFromService(service: Omit) (e) => "value" in e, ) as [PlaintextEnvVar[], SecretEnvVar[]]; - const id = + const platform = + service.labels?.[CLIENT_NAME_LABEL] === "cloud-functions" || + service.labels?.[CLIENT_NAME_LABEL] === "cloudfunctions" + ? "gcfv2" + : "run"; + + const rawId = metadata.functionId || service.annotations?.[FUNCTION_ID_ANNOTATION] || service.annotations?.[FUNCTION_TARGET_ANNOTATION] || env.find((e) => e.name === FUNCTION_TARGET_ENV)?.value || svcId; + // GCF v2 API normalizes function IDs by replacing dots with dashes (e.g. 'v2.helloWorld' + // -> 'v2-helloWorld') because GCF resource names do not allow dots. + // We must normalize IDs loaded from Cloud Run services for GCF v2 to match this behavior, + // preventing duplicate endpoints in the existing backend. We do NOT normalize for 'run' + // platform (like Dart functions) because their local specs preserve the original IDs. + // Note: GCF v2 does support underscores, so we only normalize dots. + const id = (platform === "gcfv2" ? rawId?.replace(/\./g, "-") : rawId) ?? ""; + const memory = mebibytes(service.template.containers![0]!.resources!.limits!.memory!); if (!backend.isValidMemoryOption(memory)) { logger.debug("Converting a service to an endpoint with an invalid memory option", memory); } const cpu = Number(service.template.containers![0]!.resources!.limits!.cpu); const endpoint: backend.Endpoint = { - platform: - service.labels?.[CLIENT_NAME_LABEL] === "cloud-functions" || - service.labels?.[CLIENT_NAME_LABEL] === "cloudfunctions" - ? "gcfv2" - : "run", + platform, id, project, labels: { ...service.labels, "deployment-tool": "cli-firebase" },