gcp-monorepo-secret-manager
Version:
A Google Cloud Secret Manager utility for managing environment variables across different environments and services within a monorepo.
332 lines โข 14.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GcpMonorepoSecretManager = void 0;
const app_1 = require("firebase-admin/app");
const secret_manager_1 = require("@google-cloud/secret-manager");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const ConfigManager_1 = require("./ConfigManager");
class GcpMonorepoSecretManager {
constructor(options) {
this.environment = options.environment;
this.overrideSa = options.overrideSa || false;
this.configManager = new ConfigManager_1.ConfigManager(options.configPath);
this.projectId = this.configManager.getProjectId(this.environment);
this.initializeFirebase();
this.client = this.getSecretManagerClient();
}
initializeFirebase() {
if (this.overrideSa) {
(0, app_1.initializeApp)();
return;
}
const saPath = this.configManager.getServiceAccountPath(this.environment);
if (!fs_1.default.existsSync(saPath)) {
throw new Error(`Service account file not found at ${saPath}`);
}
// Set the GOOGLE_APPLICATION_CREDENTIALS environment variable
process.env.GOOGLE_APPLICATION_CREDENTIALS = path_1.default.resolve(saPath);
const serviceAccount = JSON.parse(fs_1.default.readFileSync(path_1.default.join("./", saPath), "utf8"));
(0, app_1.initializeApp)({
credential: (0, app_1.cert)(serviceAccount),
});
// Set the project ID for other Google Cloud services
process.env.GOOGLE_CLOUD_PROJECT = serviceAccount.project_id;
}
getSecretManagerClient() {
return new secret_manager_1.SecretManagerServiceClient({
projectId: this.projectId
});
}
getEnvPath(serviceName) {
const service = this.configManager.getServiceByName(serviceName);
if (!service) {
throw new Error(`Service '${serviceName}' not found in configuration`);
}
const envSuffix = this.environment === "staging" ? "stg" : "prod";
return service.envPath.replace("{env}", envSuffix);
}
getSecretName(serviceName) {
const service = this.configManager.getServiceByName(serviceName);
if (!service) {
throw new Error(`Service '${serviceName}' not found in configuration`);
}
return `${service.secretPrefix}_ENV_FILE`;
}
getTargetPath(serviceName) {
const service = this.configManager.getServiceByName(serviceName);
if (!service) {
throw new Error(`Service '${serviceName}' not found in configuration`);
}
return service.targetPath;
}
getAvailableServices() {
return this.configManager.getServiceNames();
}
async uploadEnv(serviceName) {
if (serviceName === "all") {
const services = this.configManager.getServiceNames();
for (const service of services) {
await this.uploadSingleEnv(service);
}
return;
}
await this.uploadSingleEnv(serviceName);
}
async uploadSingleEnv(serviceName) {
const envPath = this.getEnvPath(serviceName);
if (!fs_1.default.existsSync(envPath)) {
throw new Error(`Environment file not found at ${envPath}`);
}
const envContent = fs_1.default.readFileSync(envPath, "utf8");
const secretName = this.getSecretName(serviceName);
const secretPath = `projects/${this.projectId}/secrets/${secretName}`;
try {
// Try to access the secret to see if it exists
await this.client.getSecret({ name: secretPath });
// Secret exists, add a new version
const [version] = await this.client.addSecretVersion({
parent: secretPath,
payload: {
data: Buffer.from(envContent, 'utf8'),
},
});
console.log(`Updated secret ${secretName} with new version: ${version.name}`);
// Clean up old versions after successful upload
await this.cleanupSecretVersions(secretName);
}
catch (error) {
// Secret doesn't exist, create it
if (error.message.includes('NOT_FOUND')) {
await this.client.createSecret({
parent: `projects/${this.projectId}`,
secretId: secretName,
secret: {
replication: {
automatic: {},
},
},
});
// Add the first version
const [version] = await this.client.addSecretVersion({
parent: secretPath,
payload: {
data: Buffer.from(envContent, 'utf8'),
},
});
console.log(`Created new secret ${secretName} with version: ${version.name}`);
}
else {
throw error;
}
}
console.log(`โ
${serviceName} environment file successfully uploaded to Secret Manager for ${this.environment}`);
}
/**
* Clean up old secret versions based on the delete policy
*/
async cleanupSecretVersions(secretName) {
const deletePolicy = this.configManager.getDeletePolicy();
if (!deletePolicy.enabled) {
return;
}
const secretPath = `projects/${this.projectId}/secrets/${secretName}`;
try {
// List all versions of the secret
const [versions] = await this.client.listSecretVersions({
parent: secretPath,
pageSize: 100 // Adjust if you expect more versions
});
if (!versions || versions.length <= 1) {
return; // Nothing to clean up
}
// Sort versions by creation time (newest first)
const sortedVersions = versions
.filter(v => v.state === 'ENABLED' || v.state === 'DISABLED')
.sort((a, b) => {
const timeA = new Date(a.createTime?.seconds ? Number(a.createTime.seconds) * 1000 : 0);
const timeB = new Date(b.createTime?.seconds ? Number(b.createTime.seconds) * 1000 : 0);
return timeB.getTime() - timeA.getTime();
});
const versionsToDestroy = [];
// Apply maxVersions policy
if (deletePolicy.maxVersions && deletePolicy.maxVersions > 0) {
const excessVersions = sortedVersions.slice(deletePolicy.maxVersions);
versionsToDestroy.push(...excessVersions);
}
// Apply maxAgeDays policy
if (deletePolicy.maxAgeDays && deletePolicy.maxAgeDays > 0) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - deletePolicy.maxAgeDays);
const oldVersions = sortedVersions.filter(version => {
if (!version.createTime?.seconds)
return false;
const versionDate = new Date(Number(version.createTime.seconds) * 1000);
return versionDate < cutoffDate;
});
// Ensure we don't duplicate versions already marked for deletion
oldVersions.forEach(oldVersion => {
if (!versionsToDestroy.find(v => v.name === oldVersion.name)) {
versionsToDestroy.push(oldVersion);
}
});
}
// Always keep at least 1 version
if (versionsToDestroy.length >= sortedVersions.length) {
versionsToDestroy.splice(-1, 1);
}
// Destroy the marked versions
if (versionsToDestroy.length > 0) {
console.log(`๐งน Cleaning up ${versionsToDestroy.length} old version(s) of ${secretName}`);
for (const version of versionsToDestroy) {
if (version.name) {
try {
await this.client.destroySecretVersion({
name: version.name
});
console.log(` Destroyed version: ${version.name.split('/').pop()}`);
}
catch (error) {
console.warn(` Warning: Could not destroy version ${version.name}: ${error.message}`);
}
}
}
}
}
catch (error) {
console.warn(`Warning: Could not clean up versions for ${secretName}: ${error.message}`);
}
}
/**
* Manually clean up old versions for a specific service or all services
*/
async cleanupVersions(serviceName) {
if (serviceName === "all") {
const services = this.configManager.getServiceNames();
for (const service of services) {
const secretName = this.getSecretName(service);
await this.cleanupSecretVersions(secretName);
}
console.log(`โ
Completed cleanup for all services in ${this.environment}`);
return;
}
const secretName = this.getSecretName(serviceName);
await this.cleanupSecretVersions(secretName);
console.log(`โ
Completed cleanup for ${serviceName} in ${this.environment}`);
}
async downloadEnv(serviceName) {
if (serviceName === "all") {
const services = this.configManager.getServiceNames();
for (const service of services) {
await this.downloadSingleEnv(service);
}
return;
}
await this.downloadSingleEnv(serviceName);
}
async downloadSingleEnv(serviceName) {
const envPath = this.getEnvPath(serviceName);
const secretName = this.getSecretName(serviceName);
const secretPath = `projects/${this.projectId}/secrets/${secretName}`;
try {
// Get the latest version of the env file secret
const [version] = await this.client.accessSecretVersion({
name: `${secretPath}/versions/latest`,
});
if (!version.payload || !version.payload.data) {
throw new Error(`No data found for secret ${secretName}`);
}
// Ensure the directory exists
const envDir = path_1.default.dirname(envPath);
if (!fs_1.default.existsSync(envDir)) {
fs_1.default.mkdirSync(envDir, { recursive: true });
}
// Write the entire env file content
const envContent = Buffer.from(version.payload.data).toString();
fs_1.default.writeFileSync(envPath, envContent);
console.log(`โ
Environment file successfully downloaded to ${envPath} for ${this.environment}`);
}
catch (error) {
console.error(`โ Error: Could not download environment file: ${error.message}`);
throw error;
}
}
async setEnv(serviceName) {
if (serviceName === "all") {
const services = this.configManager.getServiceNames();
for (const service of services) {
await this.setSingleEnv(service);
}
return;
}
await this.setSingleEnv(serviceName);
}
async setSingleEnv(serviceName) {
const secretName = this.getSecretName(serviceName);
const targetPath = this.getTargetPath(serviceName);
const secretPath = `projects/${this.projectId}/secrets/${secretName}`;
try {
// Get the latest version of the env file secret
const [version] = await this.client.accessSecretVersion({
name: `${secretPath}/versions/latest`,
});
if (!version.payload || !version.payload.data) {
throw new Error(`No data found for secret ${secretName}`);
}
// Ensure the target directory exists
const targetDir = path_1.default.dirname(targetPath);
if (!fs_1.default.existsSync(targetDir)) {
fs_1.default.mkdirSync(targetDir, { recursive: true });
}
// Write the entire env file content to the target path
const envContent = Buffer.from(version.payload.data).toString();
fs_1.default.writeFileSync(targetPath, envContent);
console.log(`โ
Environment file successfully set in ${targetPath} for ${this.environment}`);
}
catch (error) {
console.error(`โ Error: Could not set environment file: ${error.message}`);
throw error;
}
}
async peekEnv(serviceName) {
if (serviceName === "all") {
const services = this.configManager.getServiceNames();
for (const service of services) {
await this.peekSingleEnv(service);
}
return;
}
await this.peekSingleEnv(serviceName);
}
async peekSingleEnv(serviceName) {
const secretName = this.getSecretName(serviceName);
const secretPath = `projects/${this.projectId}/secrets/${secretName}`;
console.log(`\n๐ Environment file for ${serviceName} in ${this.environment}:\n`);
try {
// Get the latest version of the env file secret
const [version] = await this.client.accessSecretVersion({
name: `${secretPath}/versions/latest`,
});
if (!version.payload || !version.payload.data) {
throw new Error(`No data found for secret ${secretName}`);
}
// Display the entire env file content
const envContent = Buffer.from(version.payload.data).toString();
console.log(envContent);
}
catch (error) {
if (error.message.includes('NOT_FOUND')) {
console.log(`No environment file found for ${serviceName} in ${this.environment}.`);
}
else {
console.error(`โ Error: ${error.message}`);
}
}
console.log(); // Add empty line at the end
}
}
exports.GcpMonorepoSecretManager = GcpMonorepoSecretManager;
//# sourceMappingURL=GcpMonorepoSecretManager.js.map