No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

parseTriggers.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.addResourcesToBackend = exports.addResourcesToBuild = exports.mergeRequiredAPIs = exports.discoverBackend = exports.discoverBuild = exports.useStrategy = void 0;
  4. const path = require("path");
  5. const _ = require("lodash");
  6. const child_process_1 = require("child_process");
  7. const error_1 = require("../../../../error");
  8. const logger_1 = require("../../../../logger");
  9. const backend = require("../../backend");
  10. const api = require("../../../../api");
  11. const proto = require("../../../../gcp/proto");
  12. const events = require("../../../../functions/events");
  13. const functional_1 = require("../../../../functional");
  14. const TRIGGER_PARSER = path.resolve(__dirname, "./triggerParser.js");
  15. function removeInspectOptions(options) {
  16. return options.filter((opt) => !opt.startsWith("--inspect"));
  17. }
  18. function parseTriggers(projectId, sourceDir, configValues, envs) {
  19. return new Promise((resolve, reject) => {
  20. const env = Object.assign({}, envs);
  21. env.GCLOUD_PROJECT = projectId;
  22. if (!_.isEmpty(configValues)) {
  23. env.CLOUD_RUNTIME_CONFIG = JSON.stringify(configValues);
  24. }
  25. const execArgv = removeInspectOptions(process.execArgv);
  26. if (env.NODE_OPTIONS) {
  27. env.NODE_OPTIONS = removeInspectOptions(env.NODE_OPTIONS.split(" ")).join(" ");
  28. }
  29. const parser = (0, child_process_1.fork)(TRIGGER_PARSER, [sourceDir], {
  30. silent: true,
  31. env: env,
  32. execArgv: execArgv,
  33. });
  34. parser.on("message", (message) => {
  35. if (message.triggers) {
  36. resolve(message.triggers);
  37. }
  38. else if (message.error) {
  39. reject(new error_1.FirebaseError(message.error, { exit: 1 }));
  40. }
  41. });
  42. parser.on("exit", (code) => {
  43. if (code !== 0) {
  44. reject(new error_1.FirebaseError("There was an unknown problem while trying to parse function triggers.", { exit: 2 }));
  45. }
  46. });
  47. });
  48. }
  49. function useStrategy() {
  50. return Promise.resolve(true);
  51. }
  52. exports.useStrategy = useStrategy;
  53. async function discoverBuild(projectId, sourceDir, runtime, configValues, envs) {
  54. const triggerAnnotations = await parseTriggers(projectId, sourceDir, configValues, envs);
  55. const want = {
  56. requiredAPIs: [],
  57. endpoints: {},
  58. params: [],
  59. };
  60. for (const annotation of triggerAnnotations) {
  61. addResourcesToBuild(projectId, runtime, annotation, want);
  62. }
  63. return want;
  64. }
  65. exports.discoverBuild = discoverBuild;
  66. async function discoverBackend(projectId, sourceDir, runtime, configValues, envs) {
  67. const triggerAnnotations = await parseTriggers(projectId, sourceDir, configValues, envs);
  68. const want = Object.assign(Object.assign({}, backend.empty()), { environmentVariables: envs });
  69. for (const annotation of triggerAnnotations) {
  70. addResourcesToBackend(projectId, runtime, annotation, want);
  71. }
  72. return want;
  73. }
  74. exports.discoverBackend = discoverBackend;
  75. function mergeRequiredAPIs(backend) {
  76. const apiToReasons = {};
  77. for (const { api, reason } of backend.requiredAPIs) {
  78. const reasons = apiToReasons[api] || new Set();
  79. if (reason) {
  80. reasons.add(reason);
  81. }
  82. apiToReasons[api] = reasons;
  83. }
  84. const merged = [];
  85. for (const [api, reasons] of Object.entries(apiToReasons)) {
  86. merged.push({ api, reason: Array.from(reasons).join(" ") });
  87. }
  88. backend.requiredAPIs = merged;
  89. }
  90. exports.mergeRequiredAPIs = mergeRequiredAPIs;
  91. function addResourcesToBuild(projectId, runtime, annotation, want) {
  92. var _a, _b;
  93. Object.freeze(annotation);
  94. const toSeconds = (0, functional_1.nullsafeVisitor)(proto.secondsFromDuration);
  95. const regions = annotation.regions || [api.functionsDefaultRegion];
  96. let triggered;
  97. const triggerCount = +!!annotation.httpsTrigger +
  98. +!!annotation.eventTrigger +
  99. +!!annotation.taskQueueTrigger +
  100. +!!annotation.blockingTrigger;
  101. if (triggerCount !== 1) {
  102. throw new error_1.FirebaseError("Unexpected annotation generated by the Firebase Functions SDK. This should never happen.");
  103. }
  104. if (annotation.taskQueueTrigger) {
  105. want.requiredAPIs.push({
  106. api: "cloudtasks.googleapis.com",
  107. reason: "Needed for task queue functions.",
  108. });
  109. triggered = {
  110. taskQueueTrigger: {},
  111. };
  112. proto.copyIfPresent(triggered.taskQueueTrigger, annotation.taskQueueTrigger, "invoker");
  113. proto.copyIfPresent(triggered.taskQueueTrigger, annotation.taskQueueTrigger, "rateLimits");
  114. if (annotation.taskQueueTrigger.retryConfig) {
  115. triggered.taskQueueTrigger.retryConfig = {};
  116. proto.copyIfPresent(triggered.taskQueueTrigger.retryConfig, annotation.taskQueueTrigger.retryConfig, "maxAttempts", "maxDoublings");
  117. proto.convertIfPresent(triggered.taskQueueTrigger.retryConfig, annotation.taskQueueTrigger.retryConfig, "minBackoffSeconds", "minBackoff", toSeconds);
  118. proto.convertIfPresent(triggered.taskQueueTrigger.retryConfig, annotation.taskQueueTrigger.retryConfig, "maxBackoffSeconds", "maxBackoff", toSeconds);
  119. proto.convertIfPresent(triggered.taskQueueTrigger.retryConfig, annotation.taskQueueTrigger.retryConfig, "maxRetrySeconds", "maxRetryDuration", toSeconds);
  120. }
  121. }
  122. else if (annotation.httpsTrigger) {
  123. if ((_a = annotation.labels) === null || _a === void 0 ? void 0 : _a["deployment-callable"]) {
  124. delete annotation.labels["deployment-callable"];
  125. triggered = { callableTrigger: {} };
  126. }
  127. else {
  128. const trigger = {};
  129. if (annotation.failurePolicy) {
  130. logger_1.logger.warn(`Ignoring retry policy for HTTPS function ${annotation.name}`);
  131. }
  132. if (annotation.httpsTrigger.invoker) {
  133. trigger.invoker = annotation.httpsTrigger.invoker;
  134. }
  135. triggered = { httpsTrigger: trigger };
  136. }
  137. }
  138. else if (annotation.schedule) {
  139. want.requiredAPIs.push({
  140. api: "cloudscheduler.googleapis.com",
  141. reason: "Needed for scheduled functions.",
  142. });
  143. triggered = {
  144. scheduleTrigger: {
  145. schedule: annotation.schedule.schedule,
  146. timeZone: (_b = annotation.schedule.timeZone) !== null && _b !== void 0 ? _b : null,
  147. retryConfig: {},
  148. },
  149. };
  150. if (annotation.schedule.retryConfig) {
  151. triggered.scheduleTrigger.retryConfig = {};
  152. proto.copyIfPresent(triggered.scheduleTrigger.retryConfig, annotation.schedule.retryConfig, "retryCount", "maxDoublings");
  153. proto.convertIfPresent(triggered.scheduleTrigger.retryConfig, annotation.schedule.retryConfig, "maxRetrySeconds", "maxRetryDuration", toSeconds);
  154. proto.convertIfPresent(triggered.scheduleTrigger.retryConfig, annotation.schedule.retryConfig, "minBackoffSeconds", "minBackoffDuration", toSeconds);
  155. proto.convertIfPresent(triggered.scheduleTrigger.retryConfig, annotation.schedule.retryConfig, "maxBackoffSeconds", "maxBackoffDuration", toSeconds);
  156. }
  157. }
  158. else if (annotation.blockingTrigger) {
  159. if (events.v1.AUTH_BLOCKING_EVENTS.includes(annotation.blockingTrigger.eventType)) {
  160. want.requiredAPIs.push({
  161. api: "identitytoolkit.googleapis.com",
  162. reason: "Needed for auth blocking functions.",
  163. });
  164. }
  165. triggered = {
  166. blockingTrigger: {
  167. eventType: annotation.blockingTrigger.eventType,
  168. },
  169. };
  170. }
  171. else if (annotation.eventTrigger) {
  172. triggered = {
  173. eventTrigger: {
  174. eventType: annotation.eventTrigger.eventType,
  175. eventFilters: { resource: annotation.eventTrigger.resource },
  176. retry: !!annotation.failurePolicy,
  177. },
  178. };
  179. }
  180. else {
  181. throw new error_1.FirebaseError("Do not understand Cloud Function annotation without a trigger" +
  182. JSON.stringify(annotation, null, 2));
  183. }
  184. const endpointId = annotation.name;
  185. const endpoint = Object.assign({ platform: annotation.platform || "gcfv1", region: regions, project: projectId, entryPoint: annotation.entryPoint, runtime: runtime }, triggered);
  186. proto.renameIfPresent(endpoint, annotation, "serviceAccount", "serviceAccountEmail");
  187. if (annotation.vpcConnector != null) {
  188. endpoint.vpc = { connector: annotation.vpcConnector };
  189. proto.renameIfPresent(endpoint.vpc, annotation, "egressSettings", "vpcConnectorEgressSettings");
  190. }
  191. proto.copyIfPresent(endpoint, annotation, "concurrency", "labels", "maxInstances", "minInstances", "availableMemoryMb");
  192. proto.convertIfPresent(endpoint, annotation, "ingressSettings", (str) => {
  193. if (str === null) {
  194. return null;
  195. }
  196. if (!backend.AllIngressSettings.includes(str)) {
  197. throw new Error(`Invalid ingress setting ${str}`);
  198. }
  199. return str;
  200. });
  201. proto.convertIfPresent(endpoint, annotation, "timeoutSeconds", "timeout", proto.secondsFromDuration);
  202. if (annotation.secrets) {
  203. endpoint.secretEnvironmentVariables = annotation.secrets.map((secret) => {
  204. return {
  205. secret,
  206. projectId,
  207. key: secret,
  208. };
  209. });
  210. }
  211. want.endpoints[endpointId] = endpoint;
  212. }
  213. exports.addResourcesToBuild = addResourcesToBuild;
  214. function addResourcesToBackend(projectId, runtime, annotation, want) {
  215. var _a;
  216. Object.freeze(annotation);
  217. for (const region of annotation.regions || [api.functionsDefaultRegion]) {
  218. let triggered;
  219. const triggerCount = +!!annotation.httpsTrigger +
  220. +!!annotation.eventTrigger +
  221. +!!annotation.taskQueueTrigger +
  222. +!!annotation.blockingTrigger;
  223. if (triggerCount !== 1) {
  224. throw new error_1.FirebaseError("Unexpected annotation generated by the Firebase Functions SDK. This should never happen.");
  225. }
  226. if (annotation.taskQueueTrigger) {
  227. triggered = { taskQueueTrigger: annotation.taskQueueTrigger };
  228. want.requiredAPIs.push({
  229. api: "cloudtasks.googleapis.com",
  230. reason: "Needed for task queue functions.",
  231. });
  232. }
  233. else if (annotation.httpsTrigger) {
  234. if ((_a = annotation.labels) === null || _a === void 0 ? void 0 : _a["deployment-callable"]) {
  235. delete annotation.labels["deployment-callable"];
  236. triggered = { callableTrigger: {} };
  237. }
  238. else {
  239. const trigger = {};
  240. if (annotation.failurePolicy) {
  241. logger_1.logger.warn(`Ignoring retry policy for HTTPS function ${annotation.name}`);
  242. }
  243. proto.copyIfPresent(trigger, annotation.httpsTrigger, "invoker");
  244. triggered = { httpsTrigger: trigger };
  245. }
  246. }
  247. else if (annotation.schedule) {
  248. want.requiredAPIs.push({
  249. api: "cloudscheduler.googleapis.com",
  250. reason: "Needed for scheduled functions.",
  251. });
  252. triggered = { scheduleTrigger: annotation.schedule };
  253. }
  254. else if (annotation.blockingTrigger) {
  255. if (events.v1.AUTH_BLOCKING_EVENTS.includes(annotation.blockingTrigger.eventType)) {
  256. want.requiredAPIs.push({
  257. api: "identitytoolkit.googleapis.com",
  258. reason: "Needed for auth blocking functions.",
  259. });
  260. }
  261. triggered = {
  262. blockingTrigger: {
  263. eventType: annotation.blockingTrigger.eventType,
  264. options: annotation.blockingTrigger.options,
  265. },
  266. };
  267. }
  268. else {
  269. triggered = {
  270. eventTrigger: {
  271. eventType: annotation.eventTrigger.eventType,
  272. eventFilters: { resource: annotation.eventTrigger.resource },
  273. retry: !!annotation.failurePolicy,
  274. },
  275. };
  276. if (annotation.platform === "gcfv2") {
  277. if (annotation.eventTrigger.eventType === events.v2.PUBSUB_PUBLISH_EVENT) {
  278. triggered.eventTrigger.eventFilters = { topic: annotation.eventTrigger.resource };
  279. }
  280. if (events.v2.STORAGE_EVENTS.find((event) => { var _a; return event === (((_a = annotation.eventTrigger) === null || _a === void 0 ? void 0 : _a.eventType) || ""); })) {
  281. triggered.eventTrigger.eventFilters = { bucket: annotation.eventTrigger.resource };
  282. }
  283. }
  284. }
  285. const endpoint = Object.assign({ platform: annotation.platform || "gcfv1", id: annotation.name, region: region, project: projectId, entryPoint: annotation.entryPoint, runtime: runtime }, triggered);
  286. if (annotation.vpcConnector != null) {
  287. let maybeId = annotation.vpcConnector;
  288. if (maybeId && !maybeId.includes("/")) {
  289. maybeId = `projects/${projectId}/locations/${region}/connectors/${maybeId}`;
  290. }
  291. endpoint.vpc = { connector: maybeId };
  292. proto.renameIfPresent(endpoint.vpc, annotation, "egressSettings", "vpcConnectorEgressSettings");
  293. }
  294. if (annotation.secrets) {
  295. const secretEnvs = [];
  296. for (const secret of annotation.secrets) {
  297. const secretEnv = {
  298. secret,
  299. projectId,
  300. key: secret,
  301. };
  302. secretEnvs.push(secretEnv);
  303. }
  304. endpoint.secretEnvironmentVariables = secretEnvs;
  305. }
  306. proto.copyIfPresent(endpoint, annotation, "concurrency", "labels", "maxInstances", "minInstances");
  307. proto.renameIfPresent(endpoint, annotation, "serviceAccount", "serviceAccountEmail");
  308. proto.convertIfPresent(endpoint, annotation, "ingressSettings", (ingress) => {
  309. if (ingress == null) {
  310. return null;
  311. }
  312. if (!backend.AllIngressSettings.includes(ingress)) {
  313. throw new error_1.FirebaseError(`Invalid ingress setting ${ingress}`);
  314. }
  315. return ingress;
  316. });
  317. proto.convertIfPresent(endpoint, annotation, "availableMemoryMb", (mem) => {
  318. if (mem === null) {
  319. return null;
  320. }
  321. if (!backend.isValidMemoryOption(mem)) {
  322. throw new error_1.FirebaseError(`This version of firebase-tools does not know about the memory option ${mem}. Is an upgrade necessary?`);
  323. }
  324. return mem;
  325. });
  326. proto.convertIfPresent(endpoint, annotation, "timeoutSeconds", "timeout", proto.secondsFromDuration);
  327. want.endpoints[region] = want.endpoints[region] || {};
  328. want.endpoints[region][endpoint.id] = endpoint;
  329. mergeRequiredAPIs(want);
  330. }
  331. }
  332. exports.addResourcesToBackend = addResourcesToBackend;