-
Notifications
You must be signed in to change notification settings - Fork 903
/
planner.ts
233 lines (219 loc) · 8.23 KB
/
planner.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
import * as semver from "semver";
import * as extensionsApi from "../../extensions/extensionsApi";
import * as refs from "../../extensions/refs";
import { FirebaseError } from "../../error";
import {
getFirebaseProjectParams,
isLocalPath,
substituteParams,
} from "../../extensions/extensionsHelper";
import { logger } from "../../logger";
import { readInstanceParam } from "../../extensions/manifest";
import { ParamBindingOptions } from "../../extensions/paramHelper";
import { readExtensionYaml, readPostinstall } from "../../extensions/emulator/specHelper";
import { ExtensionVersion, Extension, ExtensionSpec } from "../../extensions/types";
export interface InstanceSpec {
instanceId: string;
// OneOf:
ref?: refs.Ref; // For published extensions
localPath?: string; // For local extensions
// Used by getExtensionVersion, getExtension, and getExtensionSpec.
// You should stronly prefer accessing via those methods
extensionVersion?: ExtensionVersion;
extension?: Extension;
extensionSpec?: ExtensionSpec;
}
/**
* Instance spec used by manifest.
*
* Params are passed in ParamBindingOptions so we know the param bindings for
* all environments user has configured.
*
* So far this is only used for writing to the manifest, but in the future
* we want to read manifest into this interface.
*/
export interface ManifestInstanceSpec extends InstanceSpec {
params: Record<string, ParamBindingOptions>;
}
/**
* Instance spec used for deploying extensions to firebase project or emulator.
*
* Param bindings are expected to be collapsed from ParamBindingOptions into a Record<string, string>.
*/
export interface DeploymentInstanceSpec extends InstanceSpec {
params: Record<string, string>;
allowedEventTypes?: string[];
eventarcChannel?: string;
etag?: string;
}
/**
* Caching fetcher for the corresponding ExtensionVersion for an instance spec.
*/
export async function getExtensionVersion(i: InstanceSpec): Promise<ExtensionVersion> {
if (!i.extensionVersion) {
if (!i.ref) {
throw new FirebaseError(
`Can't get ExtensionVersion for ${i.instanceId} because it has no ref`
);
}
i.extensionVersion = await extensionsApi.getExtensionVersion(refs.toExtensionVersionRef(i.ref));
}
return i.extensionVersion;
}
/**
* Caching fetcher for the corresponding Extension for an instance spec.
*/
export async function getExtension(i: InstanceSpec): Promise<Extension> {
if (!i.ref) {
throw new FirebaseError(`Can't get Extension for ${i.instanceId} because it has no ref`);
}
if (!i.extension) {
i.extension = await extensionsApi.getExtension(refs.toExtensionRef(i.ref));
}
return i.extension;
}
/** Caching fetcher for the corresponding ExtensionSpec for an instance spec.
*/
export async function getExtensionSpec(i: InstanceSpec): Promise<ExtensionSpec> {
if (!i.extensionSpec) {
if (i.ref) {
const extensionVersion = await getExtensionVersion(i);
i.extensionSpec = extensionVersion.spec;
} else if (i.localPath) {
i.extensionSpec = await readExtensionYaml(i.localPath);
i.extensionSpec!.postinstallContent = await readPostinstall(i.localPath);
} else {
throw new FirebaseError("InstanceSpec had no ref or localPath, unable to get extensionSpec");
}
}
return i.extensionSpec!;
}
/**
* have checks a project for what extension instances are currently installed,
* and returns them as a list of instanceSpecs.
* @param projectId
*/
export async function have(projectId: string): Promise<DeploymentInstanceSpec[]> {
const instances = await extensionsApi.listInstances(projectId);
return instances.map((i) => {
const dep: DeploymentInstanceSpec = {
instanceId: i.name.split("/").pop()!,
params: i.config.params,
allowedEventTypes: i.config.allowedEventTypes,
eventarcChannel: i.config.eventarcChannel,
etag: i.etag,
};
if (i.config.extensionRef) {
const ref = refs.parse(i.config.extensionRef);
dep.ref = ref;
dep.ref.version = i.config.extensionVersion;
}
return dep;
});
}
/**
* want checks firebase.json and the extensions directory for which extensions
* the user wants installed on their project.
* @param projectId The project we are deploying to
* @param projectNumber The project number we are deploying to. Used for checking .env files.
* @param aliases An array of aliases for the project we are deploying to. Used for checking .env files.
* @param projectDir The directory containing firebase.json and extensions/
* @param extensions The extensions section of firebase.jsonm
* @param emulatorMode Whether the output will be used by the Extensions emulator.
* If true, this will check {instanceId}.env.local for params and will respect `demo-` project rules.
*/
export async function want(args: {
projectId: string;
projectNumber: string;
aliases: string[];
projectDir: string;
extensions: Record<string, string>;
emulatorMode?: boolean;
}): Promise<DeploymentInstanceSpec[]> {
const instanceSpecs: DeploymentInstanceSpec[] = [];
const errors: FirebaseError[] = [];
for (const e of Object.entries(args.extensions)) {
try {
const instanceId = e[0];
const params = readInstanceParam({
projectDir: args.projectDir,
instanceId,
projectId: args.projectId,
projectNumber: args.projectNumber,
aliases: args.aliases,
checkLocal: args.emulatorMode,
});
const autoPopulatedParams = await getFirebaseProjectParams(args.projectId, args.emulatorMode);
const subbedParams = substituteParams(params, autoPopulatedParams);
// 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 !== "")
: undefined;
const eventarcChannel = subbedParams.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"];
if (isLocalPath(e[1])) {
instanceSpecs.push({
instanceId,
localPath: e[1],
params: subbedParams,
allowedEventTypes: allowedEventTypes,
eventarcChannel: eventarcChannel,
});
} else {
const ref = refs.parse(e[1]);
ref.version = await resolveVersion(ref);
instanceSpecs.push({
instanceId,
ref,
params: subbedParams,
allowedEventTypes: allowedEventTypes,
eventarcChannel: eventarcChannel,
});
}
} catch (err: any) {
logger.debug(`Got error reading extensions entry ${e}: ${err}`);
errors.push(err as FirebaseError);
}
}
if (errors.length) {
const messages = errors.map((err) => `- ${err.message}`).join("\n");
throw new FirebaseError(`Errors while reading 'extensions' in 'firebase.json'\n${messages}`);
}
return instanceSpecs;
}
/**
* resolveVersion resolves a semver string to the max matching version.
* Exported for testing.
* @param publisherId
* @param extensionId
* @param version a semver or semver range
*/
export async function resolveVersion(ref: refs.Ref): Promise<string> {
const extensionRef = refs.toExtensionRef(ref);
const versions = await extensionsApi.listExtensionVersions(extensionRef, undefined, true);
if (versions.length === 0) {
throw new FirebaseError(`No versions found for ${extensionRef}`);
}
if (!ref.version || ref.version === "latest") {
return versions
.filter((ev) => ev.spec.version !== undefined)
.map((ev) => ev.spec.version)
.sort(semver.compare)
.pop()!;
}
const maxSatisfying = semver.maxSatisfying(
versions.map((ev) => ev.spec.version),
ref.version
);
if (!maxSatisfying) {
throw new FirebaseError(
`No version of ${extensionRef} matches requested version ${ref.version}`
);
}
return maxSatisfying;
}