test(functions): failing repro tests for declarative security managed SA bugs - #10858
test(functions): failing repro tests for declarative security managed SA bugs#10858cabljac wants to merge 1 commit into
Conversation
…A bugs Reproduces two bugs in the requireRoles managed service account flow: - createV2Function does not retry the 404 GCF returns when a freshly created service account has not propagated through IAM, and grantNewRoles never polls for SA visibility before function creation - discoverSecurityDetails only rediscovers the managed SA via deployed endpoints, so a failed first deploy orphans the SA (and its project-level role grants) and every retry creates a new one Tests intentionally fail until the bugs are fixed.
There was a problem hiding this comment.
Code Review
This pull request adds several unit tests to reproduce and document existing bugs related to service account creation, propagation, and reuse during deployment. The reviewer feedback focuses on improving the robustness of these tests by stubbing iam.getServiceAccount in beforeEach blocks to prevent other tests from failing once the fixes are implemented. Additionally, the reviewer recommends avoiding as any type assertions to adhere to the repository's TypeScript style guide, suggesting type intersections or fully-formed mock objects instead.
| generateManagedSANameStub = sinon | ||
| .stub(iam, "generateManagedServiceAccountName") | ||
| .resolves("firebase-fn-123"); |
There was a problem hiding this comment.
When the fix for reusing existing managed service accounts is implemented, discoverSecurityDetails will likely call iam.getServiceAccount to check if the SA already exists in the project. Since iam.getServiceAccount is not stubbed in the beforeEach block of describe("discoverSecurityDetails"), all other tests in this suite that call discoverSecurityDetails will attempt to make real API calls and fail.\n\nTo prevent this, stub iam.getServiceAccount in the beforeEach block to reject with a 404 error by default, representing the happy path where the SA does not yet exist.
generateManagedSANameStub = sinon\n .stub(iam, "generateManagedServiceAccountName")\n .resolves("firebase-fn-123");\n sinon.stub(iam, "getServiceAccount").rejects({ status: 404 });| const saNotFoundErr = new Error( | ||
| "Service account projects/-/serviceAccounts/firebase-fn-123@test-project.iam.gserviceaccount.com was not found. Please verify that the caller has iam.serviceAccounts.actAs permission on the service account.", | ||
| ); | ||
| (saNotFoundErr as any).status = 404; |
There was a problem hiding this comment.
Avoid using as any as an escape hatch to set the status property on the error object. This violates the repository style guide standard regarding TypeScript types.\n\nInstead, use a type intersection Error & { status?: number } to safely and cleanly add the status property.
const saNotFoundErr = new Error(\n "Service account projects/-/serviceAccounts/firebase-fn-123@test-project.iam.gserviceaccount.com was not found. Please verify that the caller has iam.serviceAccounts.actAs permission on the service account."\n ) as Error & { status?: number };\n saNotFoundErr.status = 404;References
- Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
| const getServiceAccountStub = sinon.stub(iam, "getServiceAccount").resolves({ | ||
| name: "projects/test-project/serviceAccounts/firebase-fn-123@my-proj.iam.gserviceaccount.com", | ||
| email: "firebase-fn-123@my-proj.iam.gserviceaccount.com", | ||
| } as any); |
There was a problem hiding this comment.
Avoid using as any to cast the resolved value of getServiceAccount. This violates the repository style guide standard regarding TypeScript types. Instead, provide a fully-formed ServiceAccount mock object.\n\nAdditionally, since grantNewRoles will be modified to call iam.getServiceAccount to poll for propagation, other tests in this describe block (such as "should create SA and grant roles in grantNewRoles") will also trigger this call. To prevent them from attempting real API calls and failing once the fix is implemented, consider stubbing iam.getServiceAccount in the beforeEach block of describe("declarative security phases") instead of only inside this specific test.
const getServiceAccountStub = sinon.stub(iam, "getServiceAccount").resolves({\n name: "projects/test-project/serviceAccounts/firebase-fn-123@my-proj.iam.gserviceaccount.com",\n projectId: "test-project",\n uniqueId: "123",\n email: "firebase-fn-123@my-proj.iam.gserviceaccount.com",\n displayName: "",\n etag: "",\n description: "",\n oauth2ClientId: "",\n disabled: false,\n });References
- Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
Repro tests for two bugs in the declarative security (
requireRoles) managed service account flow, found while migrating storage-resize-images. Draft on purpose: the three new tests fail until the bugs are fixed, and are written against the desired behavior so a fix can land on top of them.1. Fresh SA propagation race
First deploy with
requireRolescreates the managed SA, grants roles, then immediately creates functions. IAM SA creation is eventually consistent, so GCF rejects the brand new SA with404 ... was not found ... iam.serviceAccounts.actAs. Nothing mitigates this today:grantNewRolesnever polls for visibility (src/deploy/functions/release/fabricator.ts:98-118), and the 404 is never retried (DEFAULT_RETRY_CODES = [429, 409, 503]inexecutor.ts:22;createV2Function's catch only special-cases Cloud Run RESOURCE_EXHAUSTED,fabricator.ts:557-571).Tests:
fabricator.spec.ts"retries function creation when a freshly created service account has not yet propagated" (uses a realQueueExecutorso either a retry-code or fabricator-level fix satisfies it) and "waits for a newly created service account to be visible before returning from grantNewRoles".2. Failed first deploy orphans the SA, every retry mints a new one
The managed SA is only rediscovered via
serviceAccounton already-deployed endpoints (src/deploy/functions/prepare.ts:84-92). If function creation fails after SA creation (bug 1 reproduces this reliably), the next deploy finds noexistingManagedSAand unconditionally generates a fresh randomfirebase-fn-<10digits>name (prepare.ts:141-145,src/gcp/iam.ts:307-330). Each retry leaves another orphaned SA holding project-level role grants (Storage Admin, roles/aiplatform.user in our repro), with no cleanup path.Test:
prepare.spec.ts"reuses an existing managed service account in the project when no deployed functions reference it".Issues with full repro output: #10859 (propagation race), #10860 (SA orphaning).