-
Notifications
You must be signed in to change notification settings - Fork 903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Storage Emulator Enhancement #3809
Conversation
…e-tools into colerogers.gcs-emulator
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM but please make sure someone who is familiar with the emulator code reviews too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
|
||
export class StorageCloudFunctions { | ||
private logger = EmulatorLogger.forEmulator(Emulators.STORAGE); | ||
private functionsEmulatorInfo?: EmulatorInfo; | ||
private multicastOrigin = ""; | ||
private multicastPath = ""; | ||
private enabled = false; | ||
private client: Client = new Client({ urlPrefix: "" }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason we assign a new client object here? It seems like we always create one in the constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we only initialize in the constructor when functionsEmulator
is set, we need to have client
be be initialized to some default value (can't be null/undefined) so this seemed like a reasonable place to set a default value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with what we've done here, but from strictly pedantic standpoint, I think it make more sense to let client
be an optional member of this class and then add a guard in the dispatch
call to exit early to do nothing if we know that the functionsEmulator is not running (i.e. functionsEmulatorInfo
is undefined).
/** Cloud Event */ | ||
const ceAction = STORAGE_V2_ACTION_MAP[action]; | ||
const data = { ...objectMetadataPayload }; | ||
delete (data as any).acl; // remove this field from cloud events |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think owner
property needs to be removed.
In fact, maybe we can leverage the existing type definition at https://github.com/googleapis/google-cloudevents-nodejs/blob/main/cloud/storage/v1/StorageObjectData.ts to typecheck the data object, e.g.
const data: StorageObjectData = { ... }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is kind of funny, the emulator doesn't actually send back the full object (
export class CloudStorageObjectMetadata { |
But you made a good point, I imported StorageObjectData
, casted objectMetaDataPayload
to unknown
then to StorageObjectData
(b/c we had some type mismatches)
const cloudEvent: string = JSON.stringify({ | ||
eventType: `google.cloud.storage.object.v1.${ceAction}`, // need eventType for multicast triggerId | ||
data: ce, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh I'm surprised that this works since I expected the body of the cloudevent http call to look like:
{
specVersion: 1,
type: "google.cloud.storage.object.v1..."
source: "..."
data: { ... }
}
vs
{
data: {
specVersion: 1,
type: "google.cloud.storage.object.v1..."
source: "..."
data: { ... }
}
}
Something must be unboxing the nested data
property before sending it off to the function? Can you help me step through how this works?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like I had the changes stashed locally from branch switching, I ended up changing it to the comment below, having an eventFormat
field and changing the proto
based on that.
data, | ||
}; | ||
const cloudEvent: string = JSON.stringify({ | ||
eventType: `google.cloud.storage.object.v1.${ceAction}`, // need eventType for multicast triggerId |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm this implementation feels a bit brittle since eventType
isn't a thing in a cloudevent payload. It also makes an implicit assumption that the name of the event will always be different between legacy and cloudevent. That is actually, probably true, but it's a subtle assumption that's worth being explicit (either in code or in implementation).
Another way to implement this change would be to teach the multicast handler to understand the difference between cloudevent and legacy event payloads (either by having different endpoint, having a wrapper request body e.g. { eventFormat: "cloudevent", eventBody: body }
, etc. Do you think that is going to work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept it as eventType
since I wanted to keep multicast handler almost unchanged. But since CloudEvents
moved eventType
to type
we can rely on that to figure out our trigger key. Having a wrapper request makes sense even though it now adds CloudEvents specific logic into the functionsEmulator
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe instead of a wrapper, we just build our triggerKey like this:
const triggerKey = proto.type ? `${this.args.projectId}:${proto.type}` : `${this.args.projectId}:${proto.eventType}`;
const triggers = this.multicastTriggers[triggerKey] || [];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed offline - we will probably use the value of the Content-Type
header to differentiate cloudevent vs legacy payload.
Also, I think this already works w/o any modification, but I'd make sure that the change we make here is compatible with trigger registration:
firebase-tools/src/emulator/functionsEmulator.ts
Lines 695 to 703 in e4b9b93
addStorageTrigger(projectId: string, key: string, eventTrigger: EventTrigger): boolean { | |
logger.debug(`addStorageTrigger`, JSON.stringify({ eventTrigger })); | |
const eventTriggerId = `${projectId}:${eventTrigger.eventType}`; | |
const triggers = this.multicastTriggers[eventTriggerId] || []; | |
triggers.push(key); | |
this.multicastTriggers[eventTriggerId] = triggers; | |
return true; | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall LGTM. You may want to think about adding some test cases in https://github.com/firebase/firebase-tools/tree/master/scripts/triggers-end-to-end-tests for the new storage triggers (old legacy style and new cloudevent style if you are willing to contribute some time).
Don't merge until #3829 completes |
* storage trigger works * removing comment * cleaning up request body code * add types * fixing comments * removing wrapper, adding check on type field * using Content-Type header * linter * nit
Description
Storage Emulator Enhancement