Skip to content

Commit 6242f91

Browse files
authored
Fix bug where function deployment no-oped for functions in bad state. (#8289)
* Fix bug where function deploy skipped even when function is broken. * Add test. * Don't need this. * Add changelog. * Run formatter * Fix tests. * Formatter.
1 parent 9ccb610 commit 6242f91

File tree

9 files changed

+134
-1
lines changed

9 files changed

+134
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
- Fixed issue where `apps:init` fails to detect the output directory when it was run in a directory where `app` was the only module.
22
- Set `LOG_EXECUTION_ID=true` by default for Cloud Functions (2nd gen) to improve debugging by displaying execution IDs in logs. (#8276)
3+
- Fix bug where function deployment no-oped for functions in bad state. (#8289)
34
- Updated the Firebase Data Connect local toolkit to v1.8.4 which includes the following changes: (#8290)
45
- React hooks for mutations without args no longer require `undefined` to be passed when calling `mutate`.
56
- Fixed import resolution when `moduleResolution` is set to `bundler`.

src/deploy/functions/backend.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe("Backend", () => {
2222
entryPoint: "function",
2323
runtime: "nodejs16",
2424
codebase: projectConfig.DEFAULT_CODEBASE,
25+
state: "ACTIVE",
2526
};
2627

2728
const CLOUD_FUNCTION: Omit<gcf.CloudFunction, gcf.OutputOnlyFields> = {

src/deploy/functions/backend.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ export function isBlockingTriggered(triggered: Triggered): triggered is Blocking
341341
return {}.hasOwnProperty.call(triggered, "blockingTrigger");
342342
}
343343

344+
export type EndpointState = "ACTIVE" | "FAILED" | "DEPLOYING" | "DELETING" | "UNKONWN";
345+
344346
/**
345347
* An endpoint that serves traffic to a stack of services.
346348
* For now, this is always a Cloud Function. Future iterations may use complex
@@ -381,6 +383,9 @@ export type Endpoint = TargetIds &
381383
// This may eventually be different than id because GCF is going to start
382384
// doing name translations
383385
runServiceId?: string;
386+
387+
// State of the endpoint.
388+
state?: EndpointState;
384389
};
385390

386391
export interface RequiredAPI {

src/deploy/functions/release/planner.spec.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ describe("planner", () => {
3636
runtime: "nodejs16",
3737
entryPoint: "function",
3838
environmentVariables: {},
39+
state: "ACTIVE",
3940
} as backend.Endpoint;
4041
}
4142

@@ -137,7 +138,98 @@ describe("planner", () => {
137138
});
138139
});
139140

140-
describe("calculateRegionalChanges", () => {
141+
describe("toSkipPredicate", () => {
142+
it("should skip functions with matching hashes", () => {
143+
const wantFn = func("skip", "region");
144+
const haveFn = func("skip", "region");
145+
wantFn.hash = "same-hash";
146+
haveFn.hash = "same-hash";
147+
148+
const want = { skip: wantFn };
149+
const have = { skip: haveFn };
150+
151+
const result = planner.calculateChangesets(want, have, (e) => e.region);
152+
153+
expect(result.region.endpointsToSkip).to.have.lengthOf(1);
154+
expect(result.region.endpointsToSkip[0].id).to.equal("skip");
155+
});
156+
157+
it("should not skip functions with different hashes", () => {
158+
const funcWant = func("func", "region");
159+
const funcHave = func("func", "region");
160+
funcWant.hash = "local-hash";
161+
funcHave.hash = "server-hash";
162+
163+
const want = { func: funcWant };
164+
const have = { func: funcHave };
165+
166+
const result = planner.calculateChangesets(want, have, (e) => e.region);
167+
168+
expect(result.region.endpointsToSkip).to.have.lengthOf(0);
169+
expect(result.region.endpointsToUpdate).to.have.lengthOf(1);
170+
expect(result.region.endpointsToUpdate[0].endpoint.id).to.equal("func");
171+
});
172+
173+
it("should not skip functions with missing hash values", () => {
174+
const func1Want = func("func1", "region");
175+
const func1Have = func("func1", "region");
176+
func1Want.hash = "hash";
177+
// func1Have has no hash
178+
179+
const func2Want = func("func2", "region");
180+
const func2Have = func("func2", "region");
181+
// func2Want has no hash
182+
func2Have.hash = "hash";
183+
184+
// Neither has hash
185+
const func3Want = func("func3", "region");
186+
const func3Have = func("func3", "region");
187+
188+
const want = { func1: func1Want, func2: func2Want, func3: func3Want };
189+
const have = { func1: func1Have, func2: func2Have, func3: func3Have };
190+
191+
const result = planner.calculateChangesets(want, have, (e) => e.region);
192+
193+
expect(result.region.endpointsToSkip).to.have.lengthOf(0);
194+
expect(result.region.endpointsToUpdate).to.have.lengthOf(3);
195+
});
196+
197+
it("should not skip functions targeted by --only flag", () => {
198+
const funcWant = func("func", "region");
199+
const funcHave = func("func", "region");
200+
funcWant.hash = "same-hash";
201+
funcHave.hash = "same-hash";
202+
funcWant.targetedByOnly = true;
203+
204+
const want = { func: funcWant };
205+
const have = { func: funcHave };
206+
207+
const result = planner.calculateChangesets(want, have, (e) => e.region);
208+
209+
expect(result.region.endpointsToSkip).to.have.lengthOf(0);
210+
expect(result.region.endpointsToUpdate).to.have.lengthOf(1);
211+
expect(result.region.endpointsToUpdate[0].endpoint.id).to.equal("func");
212+
});
213+
214+
it("should not skip functions in broken state", () => {
215+
const funcWant = func("func", "region");
216+
const funcHave = func("func", "region");
217+
funcWant.hash = "same-hash";
218+
funcHave.hash = "same-hash";
219+
funcHave.state = "FAILED";
220+
221+
const want = { func: funcWant };
222+
const have = { func: funcHave };
223+
224+
const result = planner.calculateChangesets(want, have, (e) => e.region);
225+
226+
expect(result.region.endpointsToSkip).to.have.lengthOf(0);
227+
expect(result.region.endpointsToUpdate).to.have.lengthOf(1);
228+
expect(result.region.endpointsToUpdate[0].endpoint.id).to.equal("func");
229+
});
230+
});
231+
232+
describe("calculateChangesets", () => {
141233
it("passes a smoke test", () => {
142234
const created = func("created", "region");
143235
const updated = func("updated", "region");
@@ -291,6 +383,24 @@ describe("planner", () => {
291383
},
292384
});
293385
});
386+
387+
it("logs a message when skipping functions", () => {
388+
// Create functions with matching hashes that will be skipped
389+
const skipWant = func("skip", "region");
390+
const skipHave = func("skip", "region");
391+
skipWant.hash = "hash";
392+
skipHave.hash = "hash";
393+
394+
const want = { skip: skipWant };
395+
const have = { skip: skipHave };
396+
397+
planner.calculateChangesets(want, have, (e) => e.region);
398+
399+
expect(logLabeledBullet).to.have.been.calledWith(
400+
"functions",
401+
"Skipping the deploy of unchanged functions.",
402+
);
403+
});
294404
});
295405

296406
describe("createDeploymentPlan", () => {

src/deploy/functions/release/planner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export function calculateChangesets(
5858
const toSkipPredicate = (id: string): boolean =>
5959
!!(
6060
!want[id].targetedByOnly && // Don't skip the function if its --only targeted.
61+
have[id].state === "ACTIVE" && // Only skip the function if its in a known good state
6162
have[id].hash &&
6263
want[id].hash &&
6364
want[id].hash === have[id].hash

src/gcp/cloudfunctions.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe("cloudfunctions", () => {
2424
entryPoint: "function",
2525
runtime: "nodejs16",
2626
codebase: projectConfig.DEFAULT_CODEBASE,
27+
state: "ACTIVE",
2728
};
2829

2930
const CLOUD_FUNCTION: Omit<cloudfunctions.CloudFunction, cloudfunctions.OutputOnlyFields> = {

src/gcp/cloudfunctions.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,18 @@ export function endpointFromFunction(gcfFunction: CloudFunction): backend.Endpoi
576576
if (gcfFunction.labels?.[HASH_LABEL]) {
577577
endpoint.hash = gcfFunction.labels[HASH_LABEL];
578578
}
579+
proto.convertIfPresent(endpoint, gcfFunction, "state", "status", (status) => {
580+
if (status === "ACTIVE") {
581+
return "ACTIVE";
582+
} else if (status === "OFFLINE") {
583+
return "FAILED";
584+
} else if (status === "DEPLOY_IN_PROGRESS") {
585+
return "DEPLOYING";
586+
} else if (status === "DELETE_IN_PROGRESS") {
587+
return "DELETING";
588+
}
589+
return "UNKONWN";
590+
});
579591
return endpoint;
580592
}
581593

src/gcp/cloudfunctionsv2.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ describe("cloudfunctionsv2", () => {
3131
codebase: projectConfig.DEFAULT_CODEBASE,
3232
runServiceId: "service",
3333
source: { storageSource: CLOUD_FUNCTION_V2_SOURCE },
34+
state: "ACTIVE",
3435
};
3536

3637
const CLOUD_FUNCTION_V2: cloudfunctionsv2.InputCloudFunction = {

src/gcp/cloudfunctionsv2.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,5 +795,6 @@ export function endpointFromFunction(gcfFunction: OutputCloudFunction): backend.
795795
if (gcfFunction.labels?.[HASH_LABEL]) {
796796
endpoint.hash = gcfFunction.labels[HASH_LABEL];
797797
}
798+
proto.copyIfPresent(endpoint, gcfFunction, "state");
798799
return endpoint;
799800
}

0 commit comments

Comments
 (0)