import ejs from "ejs"; import path from "path"; import fs from "fs/promises"; import { fileExists } from "../../files"; import { readPulumiProjectMetadata } from "../../resourceGroups"; import { execPromise } from "../../exec"; import { BaseTemplate } from "@/templates"; import { ZodObject } from "zod"; class FullstackAwsResourceGroup extends BaseTemplate { public data: any; constructor( public name: string, public directory: string ) { const dirname = path.resolve(__dirname); super(name, directory, undefined, dirname); } public async dependsOn() { const files = [ "components/app-fargate.ts", "components/cdn-cloudfront.ts", "components/lb-alb-public.ts", "components/vpc-public.ts", "utils/deep-merge.ts", "utils/prefixed.ts" ]; const packages = ["@pulumi/aws", "@pulumi/awsx", "@pulumi/pulumi", "zod"]; if (this.data.includeStorage !== "none") { files.push("components/storage-s3.ts"); } if (this.data.includeDb) { files.push("components/db-postgres-rds.ts"); packages.push("@pulumi/random"); } if (this.data.networkType === "private") { files.push("components/bastion-ec2.ts"); this.scripts[`tunnel:${this.name}`] = `infrastructure/scripts/db-tunnel.sh ${this.name}`; } return { files, packages }; } public async generate() { const renderIndex = async () => { const indexFile: string = await ejs.renderFile( path.join(__dirname, "index.ejs.t"), { ...this.data } ); const basePath = path.resolve(this.directory); const resourceGroupsPath = path.join( basePath, "infrastructure", "resourceGroups", this.name ); await fs.writeFile( path.join(resourceGroupsPath, "index.ts"), indexFile, "utf-8" ); }; const updateDockerignore = async () => { if (await fileExists(".dockerignore")) { // see if infrastructure is already in the .dockerignore file const dockerignore = await fs.readFile(".dockerignore", "utf-8"); if (!dockerignore.includes("infrastructure")) { await fs.appendFile(".dockerignore", "\ninfrastructure\n"); } } else { await fs.writeFile( ".dockerignore", [ "Dockerfile", ".dockerignore", "README.md", ".git", ".env*", "infrastructure/" ].join("\n") ); } }; const copyFiles = async () => { const basePath = path.resolve(this.directory); const scriptsPath = path.join(basePath, "infrastructure", "scripts"); if (this.data.includeDb === false || this.data.networkType === "public") { return; } // copy the scripts from the scripts folder await fs.mkdir(scriptsPath, { recursive: true }); await fs.copyFile( path.join(__dirname, "scripts", "db-tunnel.sh"), `${scriptsPath}/db-tunnel.sh` ); }; await Promise.all([renderIndex(), updateDockerignore(), copyFiles()]); } public async envPull({ resourceGroupName, stack }: { resourceGroupName: string; stack: string; }) { const metadata = await readPulumiProjectMetadata({ resourceGroupName }); const resourceGroupInputs = metadata.resourceGroupInputs; const properties = {}; if (resourceGroupInputs.includeStorage) { const { stdout: BUCKET_NAME } = await execPromise( `pulumi stack output objectStorageBucket --stack ${stack}`, { cwd: path.join("infrastructure", "resourceGroups", resourceGroupName) } ); Object.assign(properties, { BUCKET_NAME }); } if (resourceGroupInputs.includeDb) { const { stdout: secretId } = await execPromise( `pulumi stack output dbSecret --stack ${stack}`, { cwd: path.join("infrastructure", "resourceGroups", resourceGroupName) } ); const { stdout: secretResponse } = await execPromise( `aws secretsmanager get-secret-value --secret-id ${secretId}` ); const DATABASE_URL = JSON.parse(secretResponse).SecretString; if (!DATABASE_URL) { throw new Error(`Could not get secret value for ${secretId}`); } Object.assign(properties, { DATABASE_URL }); } await fs.writeFile( ".env", Object.entries(properties) .map(([key, value]) => `${key}=${value}`.trim()) .join("\n") ); } } export default FullstackAwsResourceGroup;