Skip to content
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

Add support for system params when deploying extensions #5414

Merged
merged 6 commits into from
Jan 13, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/commands/ext-configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const command = new Command("ext:configure <extensionInstanceId>")
});

const [immutableParams, tbdParams] = partition(
spec.params,
spec.params.concat(spec.systemParams ?? []),
(param) => param.immutable ?? false
);
infoImmutableParams(immutableParams, oldParamValues);
Expand Down
2 changes: 1 addition & 1 deletion src/commands/ext-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ async function installToManifest(options: InstallExtensionOptions): Promise<void

const paramBindingOptions = await paramHelper.getParams({
projectId,
paramSpecs: spec.params,
paramSpecs: spec.params.concat(spec.systemParams ?? []),
nonInteractive,
paramsEnvPath,
instanceId,
Expand Down
26 changes: 16 additions & 10 deletions src/deploy/extensions/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
} from "../../extensions/extensionsHelper";
import { logger } from "../../logger";
import { readInstanceParam } from "../../extensions/manifest";
import { ParamBindingOptions } from "../../extensions/paramHelper";
import { isSystemParam, ParamBindingOptions } from "../../extensions/paramHelper";
import { readExtensionYaml, readPostinstall } from "../../extensions/emulator/specHelper";
import { ExtensionVersion, Extension, ExtensionSpec } from "../../extensions/types";
import { partitionRecord } from "../../functional";

export interface InstanceSpec {
instanceId: string;
Expand Down Expand Up @@ -46,6 +47,7 @@ export interface ManifestInstanceSpec extends InstanceSpec {
*/
export interface DeploymentInstanceSpec extends InstanceSpec {
params: Record<string, string>;
systemParams: Record<string, string>;
allowedEventTypes?: string[];
eventarcChannel?: string;
etag?: string;
Expand Down Expand Up @@ -107,6 +109,7 @@ export async function have(projectId: string): Promise<DeploymentInstanceSpec[]>
const dep: DeploymentInstanceSpec = {
instanceId: i.name.split("/").pop()!,
params: i.config.params,
systemParams: i.config.systemParams ?? {},
allowedEventTypes: i.config.allowedEventTypes,
eventarcChannel: i.config.eventarcChannel,
etag: i.etag,
Expand Down Expand Up @@ -145,7 +148,7 @@ export async function want(args: {
try {
const instanceId = e[0];

const params = readInstanceParam({
const rawParams = readInstanceParam({
projectDir: args.projectDir,
instanceId,
projectId: args.projectId,
Expand All @@ -154,26 +157,28 @@ export async function want(args: {
checkLocal: args.emulatorMode,
});
const autoPopulatedParams = await getFirebaseProjectParams(args.projectId, args.emulatorMode);
const subbedParams = substituteParams(params, autoPopulatedParams);
const subbedParams = substituteParams(rawParams, autoPopulatedParams);
const [systemParams, params] = partitionRecord(subbedParams, isSystemParam);

// ALLOWED_EVENT_TYPES can be undefined (user input not provided) or empty string (no events selected).
// If empty string, we want to pass an empty array. If it's undefined we want to pass through undefined.
const allowedEventTypes =
subbedParams.ALLOWED_EVENT_TYPES !== undefined
? subbedParams.ALLOWED_EVENT_TYPES.split(",").filter((e) => e !== "")
params.ALLOWED_EVENT_TYPES !== undefined
? params.ALLOWED_EVENT_TYPES.split(",").filter((e) => e !== "")
: undefined;
const eventarcChannel = subbedParams.EVENTARC_CHANNEL;
const eventarcChannel = params.EVENTARC_CHANNEL;

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

if (isLocalPath(e[1])) {
instanceSpecs.push({
instanceId,
localPath: e[1],
params: subbedParams,
params,
systemParams,
allowedEventTypes: allowedEventTypes,
eventarcChannel: eventarcChannel,
});
Expand All @@ -183,7 +188,8 @@ export async function want(args: {
instanceSpecs.push({
instanceId,
ref,
params: subbedParams,
params,
systemParams,
allowedEventTypes: allowedEventTypes,
eventarcChannel: eventarcChannel,
});
Expand Down
6 changes: 6 additions & 0 deletions src/deploy/extensions/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function createExtensionInstanceTask(
projectId,
instanceId: instanceSpec.instanceId,
params: instanceSpec.params,
systemParams: instanceSpec.systemParams,
extensionVersionRef: refs.toExtensionVersionRef(instanceSpec.ref),
allowedEventTypes: instanceSpec.allowedEventTypes,
eventarcChannel: instanceSpec.eventarcChannel,
Expand All @@ -60,6 +61,7 @@ export function createExtensionInstanceTask(
projectId,
instanceId: instanceSpec.instanceId,
params: instanceSpec.params,
systemParams: instanceSpec.systemParams,
extensionSource,
allowedEventTypes: instanceSpec.allowedEventTypes,
eventarcChannel: instanceSpec.eventarcChannel,
Expand Down Expand Up @@ -92,6 +94,7 @@ export function updateExtensionInstanceTask(
instanceId: instanceSpec.instanceId,
extRef: refs.toExtensionVersionRef(instanceSpec.ref!),
params: instanceSpec.params,
systemParams: instanceSpec.systemParams,
canEmitEvents: !!instanceSpec.allowedEventTypes,
allowedEventTypes: instanceSpec.allowedEventTypes,
eventarcChannel: instanceSpec.eventarcChannel,
Expand All @@ -104,6 +107,8 @@ export function updateExtensionInstanceTask(
instanceId: instanceSpec.instanceId,
extensionSource,
validateOnly,
params: instanceSpec.params,
systemParams: instanceSpec.systemParams,
canEmitEvents: !!instanceSpec.allowedEventTypes,
allowedEventTypes: instanceSpec.allowedEventTypes,
eventarcChannel: instanceSpec.eventarcChannel,
Expand Down Expand Up @@ -134,6 +139,7 @@ export function configureExtensionInstanceTask(
projectId,
instanceId: instanceSpec.instanceId,
params: instanceSpec.params,
systemParams: instanceSpec.systemParams,
canEmitEvents: !!instanceSpec.allowedEventTypes,
allowedEventTypes: instanceSpec.allowedEventTypes,
eventarcChannel: instanceSpec.eventarcChannel,
Expand Down
25 changes: 21 additions & 4 deletions src/extensions/extensionsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,15 @@ export async function createInstance(args: {
instanceId: string;
extensionSource?: ExtensionSource;
extensionVersionRef?: string;
params: { [key: string]: string };
params: Record<string, string>;
systemParams?: Record<string, string>;
allowedEventTypes?: string[];
eventarcChannel?: string;
validateOnly?: boolean;
}): Promise<ExtensionInstance> {
const config: any = {
params: args.params,
systemParams: args.systemParams ?? {},
allowedEventTypes: args.allowedEventTypes,
eventarcChannel: args.eventarcChannel,
};
Expand Down Expand Up @@ -188,7 +190,8 @@ export async function listInstances(projectId: string): Promise<ExtensionInstanc
export async function configureInstance(args: {
projectId: string;
instanceId: string;
params: { [option: string]: string };
params: Record<string, string>;
systemParams?: Record<string, string>;
canEmitEvents: boolean;
allowedEventTypes?: string[];
eventarcChannel?: string;
Expand All @@ -215,6 +218,10 @@ export async function configureInstance(args: {
reqBody.data.config.eventarcChannel = args.eventarcChannel;
}
reqBody.updateMask += ",config.allowed_event_types,config.eventarc_channel";
if (args.systemParams) {
reqBody.data.config.systemParams = args.systemParams;
reqBody.updateMask += ",config.system_params";
}
return patchInstance(reqBody);
}

Expand All @@ -233,7 +240,8 @@ export async function updateInstance(args: {
projectId: string;
instanceId: string;
extensionSource: ExtensionSource;
params?: { [option: string]: string };
params?: Record<string, string>;
systemParams?: Record<string, string>;
canEmitEvents: boolean;
allowedEventTypes?: string[];
eventarcChannel?: string;
Expand All @@ -249,6 +257,10 @@ export async function updateInstance(args: {
body.config.params = args.params;
updateMask += ",config.params";
}
if (args.systemParams) {
body.config.systemParams = args.systemParams;
updateMask += ",config.system_params";
}
if (args.canEmitEvents) {
if (args.allowedEventTypes === undefined || args.eventarcChannel === undefined) {
throw new FirebaseError(
Expand Down Expand Up @@ -283,7 +295,8 @@ export async function updateInstanceFromRegistry(args: {
projectId: string;
instanceId: string;
extRef: string;
params?: { [option: string]: string };
params?: Record<string, string>;
systemParams?: Record<string, string>;
canEmitEvents: boolean;
allowedEventTypes?: string[];
eventarcChannel?: string;
Expand All @@ -301,6 +314,10 @@ export async function updateInstanceFromRegistry(args: {
body.config.params = args.params;
updateMask += ",config.params";
}
if (args.systemParams) {
body.config.systemParams = args.systemParams;
updateMask += ",config.system_params";
}
if (args.canEmitEvents) {
if (args.allowedEventTypes === undefined || args.eventarcChannel === undefined) {
throw new FirebaseError(
Expand Down
5 changes: 5 additions & 0 deletions src/extensions/paramHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,8 @@ export function readEnvFile(envPath: string): Record<string, string> {
}
return result.envs;
}

export function isSystemParam(paramName: string): boolean {
const regex = /^firebaseextensions\.[a-zA-Z0-9\.]*\//;
return regex.test(paramName);
}
6 changes: 3 additions & 3 deletions src/extensions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,8 @@ export interface ExtensionConfig {
name: string;
createTime: string;
source: ExtensionSource;
params: {
[key: string]: any;
};
params: Record<string, string>;
systemParams: Record<string, string>;
populatedPostinstallContent?: string;
extensionRef?: string;
extensionVersion?: string;
Expand Down Expand Up @@ -100,6 +99,7 @@ export interface ExtensionSpec {
releaseNotesUrl?: string;
sourceUrl?: string;
params: Param[];
systemParams: Param[];
preinstallContent?: string;
postinstallContent?: string;
readmeContent?: string;
Expand Down
24 changes: 21 additions & 3 deletions src/functional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,20 +97,38 @@ export function assertExhaustive(val: never): never {
}

/**
* Utility to partition an array into two based on callbackFn's truthiness for each element.
* Utility to partition an array into two based on predicate's truthiness for each element.
* Returns a Array containing two Array<T>. The first array contains all elements that returned true,
* the second contains all elements that returned false.
*/
export function partition<T>(arr: T[], callbackFn: (elem: T) => boolean): [T[], T[]] {
export function partition<T>(arr: T[], predicate: (elem: T) => boolean): [T[], T[]] {
return arr.reduce<[T[], T[]]>(
(acc, elem) => {
acc[callbackFn(elem) ? 0 : 1].push(elem);
acc[predicate(elem) ? 0 : 1].push(elem);
return acc;
},
[[], []]
);
}

/**
* Utility to partition a Record into two based on predicate's truthiness for each element.
* Returns a Array containing two Record<string, T>. The first array contains all elements that returned true,
* the second contains all elements that returned false.
*/
export function partitionRecord<T>(
rec: Record<string, T>,
predicate: (key: string, val: T) => boolean
): [Record<string, T>, Record<string, T>] {
return Object.entries(rec).reduce<[Record<string, T>, Record<string, T>]>(
(acc, [key, val]) => {
acc[predicate(key, val) ? 0 : 1][key] = val;
return acc;
},
[{}, {}]
);
}

/**
* Create a map of transformed values for all keys.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/functions/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const RESERVED_KEYS = [
const LINE_RE = new RegExp(
"^" + // begin line
"\\s*" + // leading whitespaces
"(\\w+)" + // key
"([\\w./]+)" + // key
"\\s*=[\\f\\t\\v]*" + // separator (=)
"(" + // begin optional value
"\\s*'(?:\\\\'|[^'])*'|" + // single quoted or
Expand Down
4 changes: 4 additions & 0 deletions src/test/deploy/extensions/planner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function extensionVersion(version?: string): any {
resources: [],
sourceUrl: "https://google.com",
params: [],
systemParam: [],
},
};
}
Expand Down Expand Up @@ -105,6 +106,7 @@ describe("Extensions Deployment Planner", () => {
name: "",
sourceUrl: "",
params: [],
systemParams: [],
};

const INSTANCE_WITH_EVENTS: ExtensionInstance = {
Expand All @@ -116,6 +118,7 @@ describe("Extensions Deployment Planner", () => {
etag: "123456",
config: {
params: {},
systemParams: {},
extensionRef: "firebase/image-resizer",
extensionVersion: "0.1.0",
name: "projects/my-test-proj/instances/image-resizer/configurations/95355951-397f-4821-a5c2-9c9788b2cc63",
Expand All @@ -135,6 +138,7 @@ describe("Extensions Deployment Planner", () => {
const INSTANCE_SPEC_WITH_EVENTS: planner.DeploymentInstanceSpec = {
instanceId: "image-resizer",
params: {},
systemParams: {},
allowedEventTypes: ["google.firebase.custom-event-occurred"],
eventarcChannel: "projects/my-test-proj/locations/us-central1/channels/firebase",
etag: "123456",
Expand Down
2 changes: 2 additions & 0 deletions src/test/emulators/extensions/validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function fakeInstanceSpecWithAPI(instanceId: string, apiName: string): Deploymen
return {
instanceId,
params: {},
systemParams: {},
ref: {
publisherId: "test",
extensionId: "test",
Expand All @@ -47,6 +48,7 @@ function fakeInstanceSpecWithAPI(instanceId: string, apiName: string): Deploymen
sourceUrl: "test.com",
resources: [],
params: [],
systemParams: [],
apis: [{ apiName, reason: "because" }],
},
},
Expand Down
2 changes: 2 additions & 0 deletions src/test/emulators/extensionsEmulator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const TEST_EXTENSION_VERSION: ExtensionVersion = {
},
],
params: [],
systemParams: [],
version: "0.1.18",
sourceUrl: "https://fake.test",
},
Expand Down Expand Up @@ -80,6 +81,7 @@ describe("Extensions Emulator", () => {
"google.firebase.image-resize-started,google.firebase.image-resize-completed",
EVENTARC_CHANNEL: "projects/test-project/locations/us-central1/channels/firebase",
},
systemParams: {},
allowedEventTypes: [
"google.firebase.image-resize-started",
"google.firebase.image-resize-completed",
Expand Down
2 changes: 2 additions & 0 deletions src/test/emulators/functionsEmulatorShared.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ describe("FunctionsEmulatorShared", () => {
resources: [],
sourceUrl: "test.com",
params: [],
systemParams: [],
postinstallContent: "Should subsitute ${param:KEY}",
};
const testSubbedSpec: ExtensionSpec = {
Expand All @@ -166,6 +167,7 @@ describe("FunctionsEmulatorShared", () => {
resources: [],
sourceUrl: "test.com",
params: [],
systemParams: [],
postinstallContent: "Should subsitute value",
};
const testExtension: Extension = {
Expand Down
Loading