Skip to content

Commit 1756213

Browse files
authored
Add support for system params when deploying extensions (#5414)
* Add support for configuring system params when deploying extensions * Allow . and / in env keys * Make partitionRecord more flexible
1 parent 177c99d commit 1756213

26 files changed

+178
-26
lines changed

src/commands/ext-configure.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const command = new Command("ext:configure <extensionInstanceId>")
7878
});
7979

8080
const [immutableParams, tbdParams] = partition(
81-
spec.params,
81+
spec.params.concat(spec.systemParams ?? []),
8282
(param) => param.immutable ?? false
8383
);
8484
infoImmutableParams(immutableParams, oldParamValues);

src/commands/ext-install.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ async function installToManifest(options: InstallExtensionOptions): Promise<void
212212

213213
const paramBindingOptions = await paramHelper.getParams({
214214
projectId,
215-
paramSpecs: spec.params,
215+
paramSpecs: spec.params.concat(spec.systemParams ?? []),
216216
nonInteractive,
217217
paramsEnvPath,
218218
instanceId,

src/deploy/extensions/planner.ts

+16-10
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import {
1010
} from "../../extensions/extensionsHelper";
1111
import { logger } from "../../logger";
1212
import { readInstanceParam } from "../../extensions/manifest";
13-
import { ParamBindingOptions } from "../../extensions/paramHelper";
13+
import { isSystemParam, ParamBindingOptions } from "../../extensions/paramHelper";
1414
import { readExtensionYaml, readPostinstall } from "../../extensions/emulator/specHelper";
1515
import { ExtensionVersion, Extension, ExtensionSpec } from "../../extensions/types";
16+
import { partitionRecord } from "../../functional";
1617

1718
export interface InstanceSpec {
1819
instanceId: string;
@@ -46,6 +47,7 @@ export interface ManifestInstanceSpec extends InstanceSpec {
4647
*/
4748
export interface DeploymentInstanceSpec extends InstanceSpec {
4849
params: Record<string, string>;
50+
systemParams: Record<string, string>;
4951
allowedEventTypes?: string[];
5052
eventarcChannel?: string;
5153
etag?: string;
@@ -107,6 +109,7 @@ export async function have(projectId: string): Promise<DeploymentInstanceSpec[]>
107109
const dep: DeploymentInstanceSpec = {
108110
instanceId: i.name.split("/").pop()!,
109111
params: i.config.params,
112+
systemParams: i.config.systemParams ?? {},
110113
allowedEventTypes: i.config.allowedEventTypes,
111114
eventarcChannel: i.config.eventarcChannel,
112115
etag: i.etag,
@@ -145,7 +148,7 @@ export async function want(args: {
145148
try {
146149
const instanceId = e[0];
147150

148-
const params = readInstanceParam({
151+
const rawParams = readInstanceParam({
149152
projectDir: args.projectDir,
150153
instanceId,
151154
projectId: args.projectId,
@@ -154,26 +157,28 @@ export async function want(args: {
154157
checkLocal: args.emulatorMode,
155158
});
156159
const autoPopulatedParams = await getFirebaseProjectParams(args.projectId, args.emulatorMode);
157-
const subbedParams = substituteParams(params, autoPopulatedParams);
160+
const subbedParams = substituteParams(rawParams, autoPopulatedParams);
161+
const [systemParams, params] = partitionRecord(subbedParams, isSystemParam);
158162

159163
// ALLOWED_EVENT_TYPES can be undefined (user input not provided) or empty string (no events selected).
160164
// If empty string, we want to pass an empty array. If it's undefined we want to pass through undefined.
161165
const allowedEventTypes =
162-
subbedParams.ALLOWED_EVENT_TYPES !== undefined
163-
? subbedParams.ALLOWED_EVENT_TYPES.split(",").filter((e) => e !== "")
166+
params.ALLOWED_EVENT_TYPES !== undefined
167+
? params.ALLOWED_EVENT_TYPES.split(",").filter((e) => e !== "")
164168
: undefined;
165-
const eventarcChannel = subbedParams.EVENTARC_CHANNEL;
169+
const eventarcChannel = params.EVENTARC_CHANNEL;
166170

167171
// Remove special params that are stored in the .env file but aren't actually params specified by the publisher.
168172
// Currently, only environment variables needed for Events features are considered special params stored in .env files.
169-
delete subbedParams["EVENTARC_CHANNEL"];
170-
delete subbedParams["ALLOWED_EVENT_TYPES"];
173+
delete params["EVENTARC_CHANNEL"];
174+
delete params["ALLOWED_EVENT_TYPES"];
171175

172176
if (isLocalPath(e[1])) {
173177
instanceSpecs.push({
174178
instanceId,
175179
localPath: e[1],
176-
params: subbedParams,
180+
params,
181+
systemParams,
177182
allowedEventTypes: allowedEventTypes,
178183
eventarcChannel: eventarcChannel,
179184
});
@@ -183,7 +188,8 @@ export async function want(args: {
183188
instanceSpecs.push({
184189
instanceId,
185190
ref,
186-
params: subbedParams,
191+
params,
192+
systemParams,
187193
allowedEventTypes: allowedEventTypes,
188194
eventarcChannel: eventarcChannel,
189195
});

src/deploy/extensions/tasks.ts

+6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export function createExtensionInstanceTask(
4949
projectId,
5050
instanceId: instanceSpec.instanceId,
5151
params: instanceSpec.params,
52+
systemParams: instanceSpec.systemParams,
5253
extensionVersionRef: refs.toExtensionVersionRef(instanceSpec.ref),
5354
allowedEventTypes: instanceSpec.allowedEventTypes,
5455
eventarcChannel: instanceSpec.eventarcChannel,
@@ -60,6 +61,7 @@ export function createExtensionInstanceTask(
6061
projectId,
6162
instanceId: instanceSpec.instanceId,
6263
params: instanceSpec.params,
64+
systemParams: instanceSpec.systemParams,
6365
extensionSource,
6466
allowedEventTypes: instanceSpec.allowedEventTypes,
6567
eventarcChannel: instanceSpec.eventarcChannel,
@@ -92,6 +94,7 @@ export function updateExtensionInstanceTask(
9294
instanceId: instanceSpec.instanceId,
9395
extRef: refs.toExtensionVersionRef(instanceSpec.ref!),
9496
params: instanceSpec.params,
97+
systemParams: instanceSpec.systemParams,
9598
canEmitEvents: !!instanceSpec.allowedEventTypes,
9699
allowedEventTypes: instanceSpec.allowedEventTypes,
97100
eventarcChannel: instanceSpec.eventarcChannel,
@@ -104,6 +107,8 @@ export function updateExtensionInstanceTask(
104107
instanceId: instanceSpec.instanceId,
105108
extensionSource,
106109
validateOnly,
110+
params: instanceSpec.params,
111+
systemParams: instanceSpec.systemParams,
107112
canEmitEvents: !!instanceSpec.allowedEventTypes,
108113
allowedEventTypes: instanceSpec.allowedEventTypes,
109114
eventarcChannel: instanceSpec.eventarcChannel,
@@ -134,6 +139,7 @@ export function configureExtensionInstanceTask(
134139
projectId,
135140
instanceId: instanceSpec.instanceId,
136141
params: instanceSpec.params,
142+
systemParams: instanceSpec.systemParams,
137143
canEmitEvents: !!instanceSpec.allowedEventTypes,
138144
allowedEventTypes: instanceSpec.allowedEventTypes,
139145
eventarcChannel: instanceSpec.eventarcChannel,

src/extensions/extensionsApi.ts

+21-4
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,15 @@ export async function createInstance(args: {
7373
instanceId: string;
7474
extensionSource?: ExtensionSource;
7575
extensionVersionRef?: string;
76-
params: { [key: string]: string };
76+
params: Record<string, string>;
77+
systemParams?: Record<string, string>;
7778
allowedEventTypes?: string[];
7879
eventarcChannel?: string;
7980
validateOnly?: boolean;
8081
}): Promise<ExtensionInstance> {
8182
const config: any = {
8283
params: args.params,
84+
systemParams: args.systemParams ?? {},
8385
allowedEventTypes: args.allowedEventTypes,
8486
eventarcChannel: args.eventarcChannel,
8587
};
@@ -188,7 +190,8 @@ export async function listInstances(projectId: string): Promise<ExtensionInstanc
188190
export async function configureInstance(args: {
189191
projectId: string;
190192
instanceId: string;
191-
params: { [option: string]: string };
193+
params: Record<string, string>;
194+
systemParams?: Record<string, string>;
192195
canEmitEvents: boolean;
193196
allowedEventTypes?: string[];
194197
eventarcChannel?: string;
@@ -215,6 +218,10 @@ export async function configureInstance(args: {
215218
reqBody.data.config.eventarcChannel = args.eventarcChannel;
216219
}
217220
reqBody.updateMask += ",config.allowed_event_types,config.eventarc_channel";
221+
if (args.systemParams) {
222+
reqBody.data.config.systemParams = args.systemParams;
223+
reqBody.updateMask += ",config.system_params";
224+
}
218225
return patchInstance(reqBody);
219226
}
220227

@@ -233,7 +240,8 @@ export async function updateInstance(args: {
233240
projectId: string;
234241
instanceId: string;
235242
extensionSource: ExtensionSource;
236-
params?: { [option: string]: string };
243+
params?: Record<string, string>;
244+
systemParams?: Record<string, string>;
237245
canEmitEvents: boolean;
238246
allowedEventTypes?: string[];
239247
eventarcChannel?: string;
@@ -249,6 +257,10 @@ export async function updateInstance(args: {
249257
body.config.params = args.params;
250258
updateMask += ",config.params";
251259
}
260+
if (args.systemParams) {
261+
body.config.systemParams = args.systemParams;
262+
updateMask += ",config.system_params";
263+
}
252264
if (args.canEmitEvents) {
253265
if (args.allowedEventTypes === undefined || args.eventarcChannel === undefined) {
254266
throw new FirebaseError(
@@ -283,7 +295,8 @@ export async function updateInstanceFromRegistry(args: {
283295
projectId: string;
284296
instanceId: string;
285297
extRef: string;
286-
params?: { [option: string]: string };
298+
params?: Record<string, string>;
299+
systemParams?: Record<string, string>;
287300
canEmitEvents: boolean;
288301
allowedEventTypes?: string[];
289302
eventarcChannel?: string;
@@ -301,6 +314,10 @@ export async function updateInstanceFromRegistry(args: {
301314
body.config.params = args.params;
302315
updateMask += ",config.params";
303316
}
317+
if (args.systemParams) {
318+
body.config.systemParams = args.systemParams;
319+
updateMask += ",config.system_params";
320+
}
304321
if (args.canEmitEvents) {
305322
if (args.allowedEventTypes === undefined || args.eventarcChannel === undefined) {
306323
throw new FirebaseError(

src/extensions/paramHelper.ts

+5
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,8 @@ export function readEnvFile(envPath: string): Record<string, string> {
265265
}
266266
return result.envs;
267267
}
268+
269+
export function isSystemParam(paramName: string): boolean {
270+
const regex = /^firebaseextensions\.[a-zA-Z0-9\.]*\//;
271+
return regex.test(paramName);
272+
}

src/extensions/types.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,8 @@ export interface ExtensionConfig {
6363
name: string;
6464
createTime: string;
6565
source: ExtensionSource;
66-
params: {
67-
[key: string]: any;
68-
};
66+
params: Record<string, string>;
67+
systemParams: Record<string, string>;
6968
populatedPostinstallContent?: string;
7069
extensionRef?: string;
7170
extensionVersion?: string;
@@ -100,6 +99,7 @@ export interface ExtensionSpec {
10099
releaseNotesUrl?: string;
101100
sourceUrl?: string;
102101
params: Param[];
102+
systemParams: Param[];
103103
preinstallContent?: string;
104104
postinstallContent?: string;
105105
readmeContent?: string;

src/functional.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -97,20 +97,38 @@ export function assertExhaustive(val: never): never {
9797
}
9898

9999
/**
100-
* Utility to partition an array into two based on callbackFn's truthiness for each element.
100+
* Utility to partition an array into two based on predicate's truthiness for each element.
101101
* Returns a Array containing two Array<T>. The first array contains all elements that returned true,
102102
* the second contains all elements that returned false.
103103
*/
104-
export function partition<T>(arr: T[], callbackFn: (elem: T) => boolean): [T[], T[]] {
104+
export function partition<T>(arr: T[], predicate: (elem: T) => boolean): [T[], T[]] {
105105
return arr.reduce<[T[], T[]]>(
106106
(acc, elem) => {
107-
acc[callbackFn(elem) ? 0 : 1].push(elem);
107+
acc[predicate(elem) ? 0 : 1].push(elem);
108108
return acc;
109109
},
110110
[[], []]
111111
);
112112
}
113113

114+
/**
115+
* Utility to partition a Record into two based on predicate's truthiness for each element.
116+
* Returns a Array containing two Record<string, T>. The first array contains all elements that returned true,
117+
* the second contains all elements that returned false.
118+
*/
119+
export function partitionRecord<T>(
120+
rec: Record<string, T>,
121+
predicate: (key: string, val: T) => boolean
122+
): [Record<string, T>, Record<string, T>] {
123+
return Object.entries(rec).reduce<[Record<string, T>, Record<string, T>]>(
124+
(acc, [key, val]) => {
125+
acc[predicate(key, val) ? 0 : 1][key] = val;
126+
return acc;
127+
},
128+
[{}, {}]
129+
);
130+
}
131+
114132
/**
115133
* Create a map of transformed values for all keys.
116134
*/

src/functions/env.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const RESERVED_KEYS = [
4545
const LINE_RE = new RegExp(
4646
"^" + // begin line
4747
"\\s*" + // leading whitespaces
48-
"(\\w+)" + // key
48+
"([\\w./]+)" + // key
4949
"\\s*=[\\f\\t\\v]*" + // separator (=)
5050
"(" + // begin optional value
5151
"\\s*'(?:\\\\'|[^'])*'|" + // single quoted or

src/test/deploy/extensions/planner.spec.ts

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ function extensionVersion(version?: string): any {
1818
resources: [],
1919
sourceUrl: "https://google.com",
2020
params: [],
21+
systemParam: [],
2122
},
2223
};
2324
}
@@ -105,6 +106,7 @@ describe("Extensions Deployment Planner", () => {
105106
name: "",
106107
sourceUrl: "",
107108
params: [],
109+
systemParams: [],
108110
};
109111

110112
const INSTANCE_WITH_EVENTS: ExtensionInstance = {
@@ -116,6 +118,7 @@ describe("Extensions Deployment Planner", () => {
116118
etag: "123456",
117119
config: {
118120
params: {},
121+
systemParams: {},
119122
extensionRef: "firebase/image-resizer",
120123
extensionVersion: "0.1.0",
121124
name: "projects/my-test-proj/instances/image-resizer/configurations/95355951-397f-4821-a5c2-9c9788b2cc63",
@@ -135,6 +138,7 @@ describe("Extensions Deployment Planner", () => {
135138
const INSTANCE_SPEC_WITH_EVENTS: planner.DeploymentInstanceSpec = {
136139
instanceId: "image-resizer",
137140
params: {},
141+
systemParams: {},
138142
allowedEventTypes: ["google.firebase.custom-event-occurred"],
139143
eventarcChannel: "projects/my-test-proj/locations/us-central1/channels/firebase",
140144
etag: "123456",

src/test/emulators/extensions/validation.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ function fakeInstanceSpecWithAPI(instanceId: string, apiName: string): Deploymen
3030
return {
3131
instanceId,
3232
params: {},
33+
systemParams: {},
3334
ref: {
3435
publisherId: "test",
3536
extensionId: "test",
@@ -47,6 +48,7 @@ function fakeInstanceSpecWithAPI(instanceId: string, apiName: string): Deploymen
4748
sourceUrl: "test.com",
4849
resources: [],
4950
params: [],
51+
systemParams: [],
5052
apis: [{ apiName, reason: "because" }],
5153
},
5254
},

src/test/emulators/extensionsEmulator.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const TEST_EXTENSION_VERSION: ExtensionVersion = {
4545
},
4646
],
4747
params: [],
48+
systemParams: [],
4849
version: "0.1.18",
4950
sourceUrl: "https://fake.test",
5051
},
@@ -80,6 +81,7 @@ describe("Extensions Emulator", () => {
8081
"google.firebase.image-resize-started,google.firebase.image-resize-completed",
8182
EVENTARC_CHANNEL: "projects/test-project/locations/us-central1/channels/firebase",
8283
},
84+
systemParams: {},
8385
allowedEventTypes: [
8486
"google.firebase.image-resize-started",
8587
"google.firebase.image-resize-completed",

src/test/emulators/functionsEmulatorShared.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ describe("FunctionsEmulatorShared", () => {
158158
resources: [],
159159
sourceUrl: "test.com",
160160
params: [],
161+
systemParams: [],
161162
postinstallContent: "Should subsitute ${param:KEY}",
162163
};
163164
const testSubbedSpec: ExtensionSpec = {
@@ -166,6 +167,7 @@ describe("FunctionsEmulatorShared", () => {
166167
resources: [],
167168
sourceUrl: "test.com",
168169
params: [],
170+
systemParams: [],
169171
postinstallContent: "Should subsitute value",
170172
};
171173
const testExtension: Extension = {

0 commit comments

Comments
 (0)