@magda/scripts
Version:
Scripts for building, running, and deploying MAGDA
365 lines (332 loc) • 11.2 kB
JavaScript
import { require } from "@magda/esm-utils";
const chalk = require("chalk");
const path = require("path");
const fse = require("fs-extra");
const childProcess = require("child_process");
const uniqid = require("uniqid");
const pkg = require("./package.json");
const program = require("commander");
const COMPILER_CONFIG_MAP_NAME = "scss-compiler-config";
const COMPILER_CONFIG_MAP_KEY = "scssVars";
const CHECKING_INTERVAL = 10;
program
.version(pkg.version)
.usage("[options]")
.description(
`A tool for setting magda runtime UI scss variables. Version: ${pkg.version}`
)
.option(
"-n, --namespace [k8s namespace]",
"Specify the affecting k8s namespace. It's compulsory to avoid mistakes. \n" +
" e.g. ---namespace default \n"
)
.option(
"-m, --minikube",
"Set this switch if you want to update scss variables in a local minikube cluster. \n"
)
.option(
"-f, --file [vars.json]",
"A JSON file contains an object whose keys are scss variable name."
);
program.parse(process.argv);
const programOptions = program.opts();
run(programOptions).catch((e) => {
console.error(chalk.red(`Failed to set SCSS variable: ${e}`));
process.exit(1);
});
async function run(programOptions) {
const [namespace, vars] = await validateProgramOptions(programOptions);
const env = getEnvByClusterType(programOptions.isMinikube === true);
checkIfKubectlValid(env);
checkNamespace(env, namespace);
const [image, pullSecrets] = createConfigMap(
env,
namespace,
COMPILER_CONFIG_MAP_NAME,
{
[COMPILER_CONFIG_MAP_KEY]: JSON.stringify(vars)
}
);
console.log(
chalk.green(
`Successfully created config \`${COMPILER_CONFIG_MAP_NAME}\` in namespace \`${namespace}\`.`
)
);
console.log(
chalk.yellow(`Creating updating job in namespace \`${namespace}\`...`)
);
const jobId = createJob(env, namespace, image, pullSecrets);
console.log(
chalk.green(
`Job \`${jobId}\` in namespace \`${namespace}\` has been created.`
)
);
checkingJobProgress(env, namespace, jobId);
}
async function validateProgramOptions(options) {
if (typeof options.namespace !== "string" || options.namespace == "") {
throw new Error("Invalid `namespace` parameter.");
}
if (typeof options.file !== "string" || options.file == "") {
throw new Error("Invalid `file` parameter.");
}
const filePath = path.resolve(options.file);
if (!fse.existsSync(filePath)) {
throw new Error("The path specified by `file` does not exsit.");
}
const fileData = fse.readJSONSync(filePath);
if (typeof fileData !== "object") {
throw new Error("Invalid JSON file content.");
}
return [options.namespace, fileData];
}
function getEnvByClusterType(isMinikube = false) {
if (!isMinikube) {
return Object.assign({}, process.env);
}
const dockerEnvProcess = childProcess.execSync(
"minikube docker-env --shell bash",
{ encoding: "utf8" }
);
const dockerEnv = dockerEnvProcess
.split("\n")
.filter((line) => line.indexOf("export ") === 0)
.reduce(function (env, line) {
const match = /^export (\w+)="(.*)"$/.exec(line);
if (match) {
env[match[1]] = match[2];
}
return env;
}, {});
const env = Object.assign({}, process.env, dockerEnv);
return env;
}
function checkIfKubectlValid(env) {
try {
childProcess.execSync("kubectl", {
stdio: "ignore",
env: env
});
} catch (e) {
throw new Error(
`Failed to execute \`kubectl\` utility: ${e}\n` +
"Make sure you have install & config `kubectl` properly before try again."
);
}
}
function checkNamespace(env, namespace) {
try {
childProcess.execSync(`kubectl get namespace ${namespace}`, {
stdio: "ignore",
env: env
});
return true;
} catch (e) {
console.log(
chalk.red(
`Failed to get k8s namespace \`${namespace}\` or the namespace has not been created yet: ${e}`
)
);
return false;
}
}
function buildConfigMapTemplateObject(env, namespace, name) {
let configData;
try {
configData = childProcess.execSync(
`kubectl --namespace="${namespace}" get configmap ${COMPILER_CONFIG_MAP_NAME} -o=json`,
{
env: env
}
);
} catch (e) {
throw new Error(
`Failed to retrieve configMap data from namespace \`${namespace}\`.`
);
}
try {
configData = JSON.parse(configData);
} catch (e) {
throw new Error(
`Failed to retrieve configMap data from namespace \`${namespace}\`. Invalid response returned: ${configData}`
);
}
if (!configData.data.image) {
throw new Error("Failed to retrieve image repo info from configMap.");
}
return {
...configData,
metadata: {
name,
namespace,
annotations: {},
creationTimestamp: null
}
};
}
function createConfigMap(env, namespace, configMapName, data) {
const configObj = buildConfigMapTemplateObject(
env,
namespace,
configMapName
);
configObj.data = { ...configObj.data, ...data };
const configContent = JSON.stringify(configObj);
childProcess.execSync(`kubectl apply --namespace ${namespace} -f -`, {
input: configContent,
env: env
});
let pullSecrets;
if (configObj.data.pullSecrets) {
try {
pullSecrets = JSON.parse(configObj.data.pullSecrets);
} catch (e) {
console.log("No valid pullSecrets info found from configMap.");
}
}
return [configObj.data.image, pullSecrets];
}
function buildJobTemplateObject(namespace, jobId, image, pullSecrets) {
const manifest = {
kind: "Job",
apiVersion: "batch/v1",
metadata: {
name: jobId,
namespace,
labels: {
"magda-job-id": jobId
}
},
spec: {
template: {
metadata: {
name: "scss-compiler"
},
spec: {
containers: [
{
name: "scss-compiler",
image: image,
command: [
"node",
"/usr/src/app/component/dist/index.js"
],
env: [
{
name: "USER_ID",
value:
"00000000-0000-4000-8000-000000000000"
},
{
name: "CONTENT_API_URL",
value: "http://content-api/v0"
},
{
name: "SCSS_VARS",
valueFrom: {
configMapKeyRef: {
name: "scss-compiler-config",
key: "scssVars"
}
}
},
{
name: "JWT_SECRET",
valueFrom: {
secretKeyRef: {
name: "auth-secrets",
key: "jwt-secret"
}
}
}
],
imagePullPolicy: "Always"
}
],
restartPolicy: "Never"
}
}
}
};
if (pullSecrets && pullSecrets.length) {
manifest.spec.template.spec.imagePullSecrets = pullSecrets.map(
(item) => ({
name: item
})
);
}
return manifest;
}
function createJob(env, namespace, image, pullSecrets) {
const jobId = uniqid("scss-compiler-");
const jobObj = buildJobTemplateObject(namespace, jobId, image, pullSecrets);
const configContent = JSON.stringify(jobObj);
childProcess.execSync(`kubectl apply --namespace ${namespace} -f -`, {
input: configContent,
env: env
});
return jobId;
}
function checkingJobProgress(env, namespace, jobId) {
function getJobStatus() {
const jobDataContent = childProcess.execSync(
`kubectl --namespace="${namespace}" get job -l magda-job-id=${jobId} -o=json`,
{
env: env
}
);
const jobData = JSON.parse(jobDataContent);
if (
typeof jobData !== "object" ||
!jobData.items ||
!jobData.items.length ||
!jobData.items[0] ||
!jobData.items[0].status
) {
throw new Error(`Invaid Job Status data: ${jobDataContent}`);
}
const status = jobData.items[0].status;
if (status.succeeded >= 1) {
console.log(chalk.green(`The updating job has been completed!`));
deleteJob(env, namespace, jobId);
process.exit();
} else {
console.log("");
console.log(
`The updating job is still pending complete. Current status: `
);
console.log(`Failed Times: ${status.failed ? status.failed : 0}`);
console.log(`Active Job: ${status.active ? status.active : 0}`);
console.log(`Re-check status in ${CHECKING_INTERVAL} seconds...`);
}
}
function checkOnce() {
try {
getJobStatus();
} catch (e) {
console.log(
chalk.red(
`Error has been detected when checking Job status: ${e}`
)
);
process.exit(1);
}
}
setInterval(checkOnce, CHECKING_INTERVAL * 1000);
checkOnce();
}
function deleteJob(env, namespace, jobId) {
try {
console.log(chalk.yellow(`Deleting Job ${jobId}...`));
childProcess.execSync(
`kubectl --namespace="${namespace}" delete job ${jobId}`,
{
env: env
}
);
console.log(chalk.green(`Job ${jobId} has been deleted!`));
} catch (e) {
console.log(chalk.red(`Failed to delete job ${jobId}: ${e}`));
}
}