Skip to content

Commit

Permalink
feat(quickie): programmatic setup for quickie (#76)
Browse files Browse the repository at this point in the history
* feat(quickie): programmatic setup for quickie

* fix(quickieMigration): add in rate limiter

* feat(quickie): mem saver

* chore(readme): update readme

* fix(quickieMigration): sql syntax error
  • Loading branch information
kishore03109 authored Mar 14, 2024
1 parent 345c1a3 commit 57e91e9
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 7 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ To run this, run
`npm run amplify:migrate -- -user-id=<user-id> -repair-mode=true` in the command line.
The file would exist at `../<repo-name>` for debugging purpose.

### Quickie migration

This is a one time migration flow that is meant to be run on all sites in our repos table.

1. Obtain the list of repos in the db via the sql query populate csv by running following command:
`select repos.name, deployments.hosting_id from repos inner join deployments on deployments.site_id = repos.site_id` and put this in `list-of-repos-quickie.csv`
2. run `npm run quickie:setup`
3. run commands in `sqlcommandsQuickie.txt` on production db

### Email login migration

See [here](src/emailLogin/README.md) for the specific instructions to run the email login migration.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"lint": "eslint --ext .js --ext .jsx --ignore-path .gitignore .",
"lint-fix": "eslint --ignore-path .gitignore . --fix",
"amplify:migrate": "npx ts-node src/amplify-migration/amplifyMigrationScript.ts",
"quickie:setup": "npx ts-node src/amplify-migration/quickieMigrationScript.ts",
"jump:staging": "source .ssh/.env.staging && ssh -L 5433:$DB_HOST:5432 $SSH_USER@$SSH_HOST -i .ssh/isomercms-staging-bastion.pem",
"jump:prod": "source .ssh/.env.prod && ssh -L 5433:$DB_HOST:5432 $SSH_USER@$SSH_HOST -i .ssh/isomercms-production-bastion.pem"
},
Expand Down
27 changes: 21 additions & 6 deletions src/amplify-migration/amplifyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {
CreateAppCommand,
CreateAppCommandInput,
CreateBranchCommand,
CustomRule,
GetAppCommand,
GetAppCommandOutput,
StartJobCommand,
UpdateAppCommand,
} from "@aws-sdk/client-amplify";
Expand Down Expand Up @@ -57,7 +60,7 @@ function generateCreateBranchInput(
export async function createAmplifyBranches({
appId,
isStagingLite,
}: AmplifyAppInfo) {
}: Pick<AmplifyAppInfo, "appId" | "isStagingLite">) {
if (isStagingLite) {
await awsClient.send(generateCreateBranchInput(appId, "staging-lite"));
return;
Expand All @@ -70,7 +73,7 @@ export async function createAmplifyBranches({
export async function startReleaseJob({
appId,
isStagingLite,
}: AmplifyAppInfo) {
}: Pick<AmplifyAppInfo, "appId" | "isStagingLite">) {
const params = {
appId,
branchName: "staging-lite",
Expand All @@ -88,19 +91,31 @@ export async function startReleaseJob({
await awsClient.send(new StartJobCommand(params));
}

export async function getRedirectRules(appId: string) {
const appResp: GetAppCommandOutput = await awsClient.send(
new GetAppCommand({ appId })
);
return appResp.app?.customRules ?? [];
}
export async function createAmplifyApp(
repo_name: string,
build_spec: string,
isStagingLite: boolean = false
isStagingLite: boolean = false,
existingRedirectRules: CustomRule[] = []
): Promise<string> {
let redirectRules = [
let redirectRules: CustomRule[] = [
{
source: "/<*>",
target: "/404.html",
status: "404",
},
];

const hasCustomRedirectRules = existingRedirectRules.length > 1; // 1 is the default 404 rule
if (hasCustomRedirectRules) {
redirectRules = existingRedirectRules;
}

if (isStagingLite) {
redirectRules = [
{
Expand Down Expand Up @@ -158,10 +173,10 @@ export async function createStagingLiteBranch(
const appId = await createAmplifyApp(repoName, buildSpec, true);
}

export async function protectBranch(repoPath: string) {
export async function protectBranch(appId: string) {
const command = new UpdateAppCommand({
enableBasicAuth: true,
appId: repoPath,
appId: appId,
basicAuthCredentials: Buffer.from(
`user:${process.env.AMPLIFY_DEFAULT_PASSWORD}`
).toString("base64"),
Expand Down
1 change: 1 addition & 0 deletions src/amplify-migration/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const SQL_COMMANDS_FILE = "sqlcommands.txt";
export const ORGANIZATION_NAME = "isomerpages";
export const PERMALINK_REGEX = /^permalink: /m;
export const fileExtensionsRegex = "pdf|png|jpg|gif|tif|bmp|ico|svg";
export const SQL_COMMANDS_QUICKIE_FILE = "sqlcommandsQuickie.txt";
3 changes: 2 additions & 1 deletion src/amplify-migration/githubUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ export async function createStagingLiteBranch(repoName: string): Promise<void> {
fs.mkdirSync(stgLiteDir);

// Create staging lite branch in other repo path
await simpleGit(stgLiteDir).clone(remoteRepoUrl, stgLiteDir);

await simpleGit(stgLiteDir)
.clone(remoteRepoUrl, stgLiteDir)
.checkout("staging")
.rm(["-r", "images"])
.rm(["-r", "files"]);
Expand Down
2 changes: 2 additions & 0 deletions src/amplify-migration/list-of-repos-quickie.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
repo_name,appId
kishore-test-email-login,d1234
174 changes: 174 additions & 0 deletions src/amplify-migration/quickieMigrationScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { errorMessage } from "./errorMessage";

import fs from "fs";
import path from "path";
import csv from "csv-parser";
import os from "os";
require("dotenv").config();

import { createStagingLiteBranch, isRepoPrivate } from "./githubUtils";
import { LOGS_FILE, SQL_COMMANDS_QUICKIE_FILE } from "./constants";
import { isRepoEmpty } from "./githubUtils";
import {
createAmplifyApp,
createAmplifyBranches,
getRedirectRules,
protectBranch,
readBuildSpec,
startReleaseJob,
} from "./amplifyUtils";

interface AmplifyAppInfo {
appId: string;
repoName: string;

repoPath: string;
isStagingLite?: boolean;
}

const MAX_APP_LIMIT = 25;
// delay for 1 hour
async function delayFor1Hr() {
return new Promise((resolve) => setTimeout(resolve, 3600000));
}

async function quickifyRepo(repoName: string, stagingAppId: string) {
const pwd = process.cwd();

const repoPath = path.join(`${pwd}/../${repoName}-staging-lite`);

const buildSpec = await readBuildSpec();
const redirectRules = await getRedirectRules(stagingAppId);
// Integration with reduction in build times
await createStagingLiteBranch(repoName);
const isRepoPrivatised = await isRepoPrivate(repoName);
const stagingLiteAppId = await createAmplifyApp(
repoName,
buildSpec,
true,
redirectRules
);
const stagingLiteAppInfo: AmplifyAppInfo = {
appId: stagingLiteAppId,
repoName,

repoPath,
isStagingLite: true,
};
await createAmplifyBranches(stagingLiteAppInfo);
if (isRepoPrivatised) {
await protectBranch(stagingLiteAppId);
}
await startReleaseJob(stagingLiteAppInfo);

await updateDBForQuickie(stagingAppId, stagingLiteAppInfo);

//remove the repos from local disk to save mem
fs.rmdirSync(repoPath, { recursive: true });
}

/**
* Reading CSV file
* @param filePath if undefined, list-of-repos.csv
* in the current directory will be used
* @returns list of repos and their human friendly names
*/
function readCsvFile(
// populate csv by running following command:
// select repos.name, deployments.hosting_id from repos inner join deployments on deployments.site_id = repos.site_id
filePath = path.join(__dirname, "list-of-repos-quickie.csv")
): Promise<[string, string][]> {
return new Promise((resolve, reject) => {
const results: [string, string][] = [];
fs.createReadStream(filePath)
.pipe(csv())
.on("data", (data: { repo_name: string; appId: string }) => {
results.push([data.repo_name, data.appId]);
})
.on("end", () => {
resolve(results);
})
.on("error", (err: any) => {
reject(err);
});
});
}

async function main() {
// check for all env vars first
if (
!process.env.GITHUB_ACCESS_TOKEN ||
!process.env.AWS_ACCESS_KEY_ID ||
!process.env.AWS_SECRET_ACCESS_KEY
) {
console.error(
"Please provide all env vars: GITHUB_ACCESS_TOKEN, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY"
);
return;
}

const args = process.argv.slice(2);

const filePath = args
.find((arg) => arg.startsWith("-repo-path="))
?.split("=")[1];
const listOfRepos: [string, string][] = await readCsvFile(filePath);

// delineate logs for easier separation of runs
const delineateString = `------------------${new Date().toLocaleString()}------------------`;
fs.appendFileSync(path.join(__dirname, LOGS_FILE), delineateString + os.EOL);
fs.appendFileSync(
path.join(__dirname, SQL_COMMANDS_QUICKIE_FILE),
delineateString + os.EOL
);
let counter = MAX_APP_LIMIT; // App limit per hour
for (const [repoName, appId] of listOfRepos) {
try {
if (await isRepoEmpty(repoName)) {
console.info(`Skipping ${repoName} as it has no code`);
// write repos that have no code to a file
fs.appendFileSync(
path.join(__dirname, LOGS_FILE),
`${repoName} ` + os.EOL
);
continue;
}

await quickifyRepo(repoName, appId);
counter--;
if (counter === 0) {
console.info("Max app limit reached. Waiting for 1 hour");
await delayFor1Hr();
counter = MAX_APP_LIMIT;
}
} catch (e) {
const error: errorMessage = {
message: `${e}`,
repoName,
};
const message = `Error occurred for ${error.repoName}: ${error.message}`;
console.error(message);
// append this to a file
fs.appendFileSync(
path.join(__dirname, LOGS_FILE),
`${message} ` + os.EOL
);
}
}
}

async function updateDBForQuickie(
stagingAppId: string,
stagingLiteAppInfo: AmplifyAppInfo
) {
const sqlCommands = `update deployments set staging_lite_hosting_id = '${stagingLiteAppInfo.appId}',staging_url = 'https://staging-lite.${stagingLiteAppInfo.appId}.amplifyapp.com' where deployments.hosting_id='${stagingAppId}';\n`;
const sqlFile = path.join(__dirname, SQL_COMMANDS_QUICKIE_FILE);
// append sql commands to file
await fs.promises.appendFile(sqlFile, sqlCommands);
await fs.promises.appendFile(
path.join(__dirname, LOGS_FILE),
`${stagingLiteAppInfo.repoName}: SQL commands appended to ${sqlFile} \n`
);
}

main();

0 comments on commit 57e91e9

Please sign in to comment.