firebase-tools
Version:
Command-Line Interface for Firebase
196 lines (195 loc) • 9.48 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_LIFECYCLE_RULES = exports.DEFAULT_CORS_RULES = exports.DEFAULT_FILE_TTL_DAYS = exports.DEFAULT_BUCKET_LOCATION = exports.DEFAULT_CORS_ORIGINS = exports.HEAP_DUMP_BUCKET_PREFIX = exports.STORAGE_OBJECT_CREATOR_ROLE = exports.CRASHLYTICS_SERVICE_NAME = void 0;
exports.getCrashlyticsV1Client = getCrashlyticsV1Client;
exports.parseAndroidHashedId = parseAndroidHashedId;
exports.createBucketName = createBucketName;
exports.getCrashlyticsP4sa = getCrashlyticsP4sa;
exports.resolveAndroidAppId = resolveAndroidAppId;
exports.getProfilingManagerConfig = getProfilingManagerConfig;
exports.updateProfilingManagerConfig = updateProfilingManagerConfig;
exports.ensureHeapDumpStorageBucket = ensureHeapDumpStorageBucket;
exports.ensureHeapDumpP4saRole = ensureHeapDumpP4saRole;
exports.enableHeapDumpCollection = enableHeapDumpCollection;
exports.disableHeapDumpCollection = disableHeapDumpCollection;
const apiv2_1 = require("../apiv2");
const api_1 = require("../api");
const error_1 = require("../error");
const logger_1 = require("../logger");
const utils_1 = require("../utils");
const utils_2 = require("./utils");
const storage = require("../gcp/storage");
const resourceManager = require("../gcp/resourceManager");
const serviceusage = require("../gcp/serviceusage");
const apps_1 = require("../management/apps");
function getCrashlyticsV1Client() {
return new apiv2_1.Client({
urlPrefix: (0, api_1.crashlyticsApiOrigin)(),
apiVersion: "v1",
});
}
exports.CRASHLYTICS_SERVICE_NAME = "firebasecrashlytics.googleapis.com";
exports.STORAGE_OBJECT_CREATOR_ROLE = "roles/storage.objectCreator";
exports.HEAP_DUMP_BUCKET_PREFIX = "firebasecrashlytics-heap-dumps-";
exports.DEFAULT_CORS_ORIGINS = ["https://console.firebase.google.com"];
exports.DEFAULT_BUCKET_LOCATION = "us-east1";
exports.DEFAULT_FILE_TTL_DAYS = 90;
exports.DEFAULT_CORS_RULES = [
{
origin: exports.DEFAULT_CORS_ORIGINS,
method: ["GET", "HEAD", "OPTIONS"],
responseHeader: ["Content-Type", "Access-Control-Allow-Origin", "Content-Length"],
},
];
exports.DEFAULT_LIFECYCLE_RULES = [
{
action: { type: "Delete" },
condition: { age: exports.DEFAULT_FILE_TTL_DAYS },
},
];
const ANDROID_APP_ID_REGEX = /^\d+:(\d+):android:([a-fA-F0-9]+)$/;
function parseAndroidHashedId(appId) {
const match = ANDROID_APP_ID_REGEX.exec(appId);
if (!match) {
throw new error_1.FirebaseError(`App ID '${appId}' is not a valid Android app ID (expected format '1:<project-number>:android:<hash>'). ` +
"Heap dump collection is only supported for Android apps. " +
"Run 'firebase apps:list ANDROID' to view valid Android app IDs for your project.");
}
return match[2].toLowerCase();
}
function createBucketName(appId) {
return `${exports.HEAP_DUMP_BUCKET_PREFIX}${parseAndroidHashedId(appId)}`;
}
function getCrashlyticsP4sa(projectNumber) {
return `service-${projectNumber}@gcp-sa-crashlytics.iam.gserviceaccount.com`;
}
async function resolveAndroidAppId(projectId, options) {
if (options.app) {
parseAndroidHashedId(options.app);
return options.app;
}
const apps = await (0, apps_1.listFirebaseApps)(projectId, apps_1.AppPlatform.ANDROID);
if (apps.length === 0) {
throw new error_1.FirebaseError(`No Android apps found in project '${projectId}'. Heap dump collection is only supported for Android apps.\n` +
"You can register an Android app using 'firebase apps:create ANDROID <package-name>'");
}
if (apps.length === 1) {
return apps[0].appId;
}
if (options.nonInteractive) {
throw new error_1.FirebaseError(`Project '${projectId}' has multiple Android apps. Please specify an app ID with '--app <appID>'.`);
}
const selectedApp = await (0, apps_1.selectAppInteractively)(apps, apps_1.AppPlatform.ANDROID, {
message: "Select an Android app:",
});
return selectedApp.appId;
}
async function getProfilingManagerConfig(appId) {
const projectNumber = (0, utils_2.parseProjectNumber)(appId);
logger_1.logger.debug(`[crashlytics] getProfilingManagerConfig called with appId: ${appId}`);
const response = await getCrashlyticsV1Client().request({
method: "GET",
headers: {
"Content-Type": "application/json",
},
path: `/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`,
timeout: utils_2.TIMEOUT,
});
const config = response.body?.configuration || {};
return {
gcsBucket: config.gcsBucket || "",
heapDumpCollectionEnabled: config.heapDumpCollectionEnabled ?? false,
};
}
async function updateProfilingManagerConfig(appId, config) {
const projectNumber = (0, utils_2.parseProjectNumber)(appId);
logger_1.logger.debug(`[crashlytics] updateProfilingManagerConfig called with appId: ${appId}, config: ${JSON.stringify(config)}`);
await getCrashlyticsV1Client().request({
method: "POST",
headers: {
"Content-Type": "application/json",
},
path: `/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`,
body: {
projectNumber,
gmpAppId: appId,
configuration: {
gcsBucket: config.gcsBucket ?? "",
heapDumpCollectionEnabled: config.heapDumpCollectionEnabled ?? false,
},
},
timeout: utils_2.TIMEOUT,
});
}
async function ensureHeapDumpStorageBucket(projectId, appId, location = exports.DEFAULT_BUCKET_LOCATION) {
const bucketName = createBucketName(appId);
const projectNumber = (0, utils_2.parseProjectNumber)(appId);
try {
const bucket = await storage.getBucket(bucketName);
if (bucket.projectNumber && String(bucket.projectNumber) !== projectNumber) {
throw new error_1.FirebaseError(`Cloud Storage bucket '${bucketName}' already exists and is owned by another project.`);
}
const patchPayload = {};
const hasConsoleCors = bucket.cors?.some((rule) => exports.DEFAULT_CORS_ORIGINS.every((origin) => rule.origin?.includes(origin)));
if (!hasConsoleCors) {
patchPayload.cors = [...(bucket.cors ?? []), ...exports.DEFAULT_CORS_RULES];
}
const hasDeleteLifecycle = bucket.lifecycle?.rule?.some((rule) => rule.action.type === "Delete" && rule.condition.age !== undefined);
if (!hasDeleteLifecycle) {
patchPayload.lifecycle = {
rule: [...(bucket.lifecycle?.rule ?? []), ...exports.DEFAULT_LIFECYCLE_RULES],
};
}
if (Object.keys(patchPayload).length > 0) {
logger_1.logger.debug(`[crashlytics] Bucket ${bucketName} found without full configuration. Patching ${Object.keys(patchPayload).join(", ")}.`);
await storage.patchBucket(bucketName, patchPayload);
}
return bucketName;
}
catch (err) {
const status = (0, error_1.getErrStatus)(err);
if (status !== 404) {
throw err;
}
}
logger_1.logger.debug(`[crashlytics] Creating GCS bucket ${bucketName} in ${location}...`);
await storage.createBucket(projectId, {
name: bucketName,
location,
cors: exports.DEFAULT_CORS_RULES,
lifecycle: { rule: exports.DEFAULT_LIFECYCLE_RULES },
}, true);
return bucketName;
}
async function ensureHeapDumpP4saRole(projectId, projectNumber) {
await serviceusage.generateServiceIdentityAndPoll(projectNumber, exports.CRASHLYTICS_SERVICE_NAME, "crashlytics");
const p4saEmail = getCrashlyticsP4sa(projectNumber);
const hasRole = await resourceManager.serviceAccountHasRoles(projectId, p4saEmail, [exports.STORAGE_OBJECT_CREATOR_ROLE], true);
if (!hasRole) {
logger_1.logger.debug(`[crashlytics] Adding ${exports.STORAGE_OBJECT_CREATOR_ROLE} to ${p4saEmail}...`);
await resourceManager.addServiceAccountToRoles(projectId, p4saEmail, [exports.STORAGE_OBJECT_CREATOR_ROLE], true);
}
}
async function enableHeapDumpCollection(projectId, appId, location = exports.DEFAULT_BUCKET_LOCATION) {
const projectNumber = (0, utils_2.parseProjectNumber)(appId);
(0, utils_1.logLabeledBullet)("crashlytics", "Configuring Google Cloud Storage bucket...");
const bucketName = await ensureHeapDumpStorageBucket(projectId, appId, location);
(0, utils_1.logLabeledBullet)("crashlytics", "Configuring service agent permissions...");
await ensureHeapDumpP4saRole(projectId, projectNumber);
(0, utils_1.logLabeledBullet)("crashlytics", "Enabling Crashlytics heap dump collection...");
await updateProfilingManagerConfig(appId, {
gcsBucket: bucketName,
heapDumpCollectionEnabled: true,
});
(0, utils_1.logLabeledSuccess)("crashlytics", "Successfully enabled Crashlytics heap dump collection!");
return bucketName;
}
async function disableHeapDumpCollection(appId) {
(0, utils_1.logLabeledBullet)("crashlytics", "Disabling Crashlytics heap dump collection...");
const currentConfig = await getProfilingManagerConfig(appId);
await updateProfilingManagerConfig(appId, {
gcsBucket: currentConfig.gcsBucket,
heapDumpCollectionEnabled: false,
});
(0, utils_1.logLabeledSuccess)("crashlytics", "Successfully disabled Crashlytics heap dump collection!");
}