Skip to content

Commit

Permalink
more atomic writes, complete clobberage
Browse files Browse the repository at this point in the history
  • Loading branch information
johnstonmatt committed Jan 25, 2023
1 parent c487116 commit 788724d
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 57 deletions.
2 changes: 2 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/cndi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default async function main(args: string[]) {
const homeDirectory = homedir() || "~";

const CNDI_HOME = path.join(homeDirectory, ".cndi");

const timestamp = `${Date.now()}`;
const stagingDirectory = path.join(CNDI_HOME, "staging", timestamp);

Expand Down
23 changes: 20 additions & 3 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import getGitignoreContents from "../outputs/gitignore.ts";
import vscodeSettings from "../outputs/vscode-settings.ts";

import { Template } from "../templates/Template.ts";
import { dirname } from "https://deno.land/std@0.173.0/path/mod.ts";

const initLabel = white("init:");

Expand Down Expand Up @@ -348,10 +349,24 @@ export default async function init(context: CNDIContext) {

if (!noGitHub) {
try {
const workflowContents = await Deno.readTextFile(
path.join(CNDI_SRC, "github", "workflows", "cndi-run.yaml"),
);

const targetGithubDirectory = path.join(
".github",
"workflows",
"cndi-run.yaml",
);

await Deno.mkdir(dirname(targetGithubDirectory), { recursive: true });

// overwrite the github workflows and readme, do not clobber other files
await copy(path.join(CNDI_SRC, "github"), githubDirectory, {
overwrite: true,
});
await stageFile(
context.stagingDirectory,
targetGithubDirectory,
workflowContents,
);
} catch (githubCopyError) {
console.log(
initLabel,
Expand Down Expand Up @@ -401,6 +416,8 @@ export default async function init(context: CNDIContext) {
templateString,
);

await persistStagedFiles(context.stagingDirectory, projectDirectory);

const finalContext = {
...cndiContextWithGeneratedValues,
pathToConfig: configOutputPath,
Expand Down
100 changes: 64 additions & 36 deletions src/commands/overwrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import * as path from "https://deno.land/std@0.173.0/path/mod.ts";
import {
getPrettyJSONString,
loadJSONC,
stageFile,
persistStagedFiles,
stageFile,
stageFileSync,
} from "../utils.ts";
import {
Expand All @@ -29,7 +29,7 @@ import { loadTerraformStatePassphrase } from "../initialize/terraformStatePassph

import { loadArgoUIReadOnlyPassword } from "../initialize/argoUIReadOnlyPassword.ts";

import { getGoogleCredentials } from "../deployment-targets/gcp.ts";
import { getEnvStringWithGoogleCredentials } from "../deployment-targets/gcp.ts";

import {
brightRed,
Expand Down Expand Up @@ -83,10 +83,12 @@ const overwriteWithFn = async (context: CNDIContext, initializing = false) => {
console.log(
owLabel,
brightRed(
`you need to specify a ${cyan(
'"project_name"'
)} for your CNDI cluster, it is used to tag resources we create`
)
`you need to specify a ${
cyan(
'"project_name"',
)
} for your CNDI cluster, it is used to tag resources we create`,
),
);
Deno.exit(1);
}
Expand Down Expand Up @@ -126,20 +128,20 @@ const overwriteWithFn = async (context: CNDIContext, initializing = false) => {
await stageFile(
context.stagingDirectory,
path.join("cndi", "terraform", "leader_bootstrap_cndi.sh.tftpl"),
leaderBootstrapTerraformTemplate
leaderBootstrapTerraformTemplate,
);

await stageFile(
context.stagingDirectory,
path.join("cndi", "terraform", "controller_bootstrap_cndi.sh.tftpl"),
controllerBootstrapTerrformTemplate
controllerBootstrapTerrformTemplate,
);

// create temporary key for sealing secrets
const tempPublicKeyFilePath = await Deno.makeTempFile();
await Deno.writeTextFile(
tempPublicKeyFilePath,
sealedSecretsKeys.sealed_secrets_public_key
sealedSecretsKeys.sealed_secrets_public_key,
);

// write each manifest in the "cluster" section of the config to `cndi/cluster`
Expand All @@ -152,14 +154,14 @@ const overwriteWithFn = async (context: CNDIContext, initializing = false) => {
const sealedSecretManifest = await getSealedSecretManifest(
secret,
tempPublicKeyFilePath,
context
context,
);

if (sealedSecretManifest) {
stageFileSync(
context.stagingDirectory,
path.join("cndi", "cluster_manifests", secretName),
sealedSecretManifest
sealedSecretManifest,
);
console.log(`created encrypted secret:`, secretName);
}
Expand All @@ -169,7 +171,7 @@ const overwriteWithFn = async (context: CNDIContext, initializing = false) => {
await stageFile(
context.stagingDirectory,
path.join("cndi", "cluster_manifests", `${key}.json`),
getPrettyJSONString(manifestObj)
getPrettyJSONString(manifestObj),
);
});

Expand All @@ -193,61 +195,77 @@ const overwriteWithFn = async (context: CNDIContext, initializing = false) => {
// eg: aws -> aws
const provider = node.kind;
return provider;
})
}),
);

if (requiredProviders.size !== 1) {
console.log(
yellow(`we currently only support ${cyan("1")} "kind" per cluster\n`)
yellow(`we currently only support ${cyan("1")} "kind" per cluster\n`),
);
console.log(
`your nodes have the following ${brightRed(
`${requiredProviders.size}`
)} "kind"s:`
`your nodes have the following ${
brightRed(
`${requiredProviders.size}`,
)
} "kind"s:`,
);
requiredProviders.forEach((kind) => {
console.log(` - ${yellow(kind)}`);
});
console.log();
Deno.exit(1);
}

if (requiredProviders.has("gcp")) {
// if there is a service account key path, read the contents and write them to .env and this runtime env
// if there is a service account key path
// read the contents and create a string of .env contents with the key
// caution: this needs to run before the terraform root file is created
await getGoogleCredentials(context.dotEnvPath);
const envStringIncludingGCPCreds = getEnvStringWithGoogleCredentials(
context,
);
if (envStringIncludingGCPCreds) {
stageFileSync(
context.stagingDirectory,
".env",
envStringIncludingGCPCreds,
);
}
}

// generate setup-cndi.tf.json which depends on which kind of nodes are being deployed
const terraformRootFile = await getTerraformRootFile({
const terraformRootFileContents = getTerraformRootFile({
cndi_project_name: config.project_name,
leaderName: leader.name,
requiredProviders,
nodes,
});

// write terraform root file
await Deno.writeTextFile(
path.join(pathToTerraformResources, "setup-cndi.tf.json"),
terraformRootFile
await stageFile(
context.stagingDirectory,
path.join("cndi", "terraform", "setup-cndi.tf.json"),
terraformRootFileContents,
);

// write terraform nodes files
nodes.forEach((node: BaseNodeItemSpec) => {
const nodeFileContents: string = getTerraformNodeResource(
node,
deployment_target_configuration,
leader.name
leader.name,
);
Deno.writeTextFileSync(
path.join(pathToTerraformResources, `${node.name}.cndi-node.tf.json`),
nodeFileContents

stageFileSync(
context.stagingDirectory,
path.join("cndi", "terraform", `${node.name}.cndi-node.tf.json`),
nodeFileContents,
);
});

// write the cndi/cluster_manifests/Chart.yaml file
await Deno.writeTextFile(
path.join(pathToKubernetesManifests, "Chart.yaml"),
RootChartYaml
await stageFile(
context.stagingDirectory,
path.join("cndi", "cluster_manifests", "Chart.yaml"),
RootChartYaml,
);

const { applications } = config;
Expand All @@ -257,16 +275,26 @@ const overwriteWithFn = async (context: CNDIContext, initializing = false) => {
const applicationSpec = applications[releaseName];
const [manifestContent, filename] = getApplicationManifest(
releaseName,
applicationSpec
applicationSpec,
);
Deno.writeTextFileSync(
path.join(pathToKubernetesManifests, "applications", filename),
manifestContent
stageFileSync(
context.stagingDirectory,
path.join("cndi", "cluster_manifests", "applications", filename),
manifestContent,
);
console.log("created application manifest:", filename);
});

await persistStagedFiles(context.stagingDirectory, context.projectDirectory);
try {
await persistStagedFiles(
context.stagingDirectory,
context.projectDirectory,
);
} catch (errorPersistingStagedFiles) {
console.log(owLabel, brightRed(`Error persisting staged files`));
console.log(errorPersistingStagedFiles);
Deno.exit(1);
}

const completionMessage = initializing
? "initialized your cndi project in the ./cndi directory!"
Expand Down
21 changes: 14 additions & 7 deletions src/deployment-targets/gcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,26 @@ import {
yellow,
} from "https://deno.land/std@0.173.0/fmt/colors.ts";
import { homedir } from "https://deno.land/std@0.173.0/node/os.ts?s=homedir";

import { EnvObject } from "../types.ts";
import * as path from "https://deno.land/std@0.173.0/path/mod.ts";
import { CNDIContext, EnvObject } from "../types.ts";
import { Input } from "https://deno.land/x/cliffy@v0.25.4/prompt/mod.ts";

import { stageFileSync } from "../utils.ts";

const deploymentTargetsLabel = white("deployment-targets/gcp:");

const GCP_PATH_TO_SERVICE_ACCOUNT_KEY_ENVKEY =
"GCP_PATH_TO_SERVICE_ACCOUNT_KEY";
const GOOGLE_CREDENTIALS_ENVKEY = "GOOGLE_CREDENTIALS";

// pull contents of the gcp service account key file into the env, and write them to .env
const getGoogleCredentials = async (dotEnvPath: string) => {
const getEnvStringWithGoogleCredentials = (
context: CNDIContext,
): string | void => {
const { projectDirectory } = context;

const dotEnvPath = path.join(projectDirectory, ".env");

const gcp_path_to_service_account_key = Deno.env.get(
GCP_PATH_TO_SERVICE_ACCOUNT_KEY_ENVKEY,
);
Expand All @@ -32,7 +40,7 @@ const getGoogleCredentials = async (dotEnvPath: string) => {
// if the user interactively provides a path to a service account key, we copy it to `.env` and discard the path
if (shouldAttemptToLoadKey) {
try {
const keyText = await Deno.readTextFile(
const keyText = Deno.readTextFileSync(
gcp_path_to_service_account_key.replace("~", homedir() || "~"),
);

Expand All @@ -57,8 +65,7 @@ const getGoogleCredentials = async (dotEnvPath: string) => {

// join the array of lines back into a string
const newDotEnvContents = newDotEnvLines.join("\n");
// overwrite `.env` with the new contents
Deno.writeTextFileSync(dotEnvPath, newDotEnvContents);
return newDotEnvContents;
}
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
Expand Down Expand Up @@ -143,4 +150,4 @@ const prepareGCPEnv = async (interactive: boolean): Promise<EnvObject> => {
return gcpEnvObject;
};

export { getGoogleCredentials, prepareGCPEnv };
export { getEnvStringWithGoogleCredentials, prepareGCPEnv };
10 changes: 2 additions & 8 deletions src/outputs/terraform-root-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ interface GetTerraformRootFileArgs {

const terraformRootFileLabel = white("outputs/terraform-root-file:");

const getTerraformRootFile = async ({
const getTerraformRootFile = ({
leaderName,
requiredProviders,
nodes,
cndi_project_name,
}: GetTerraformRootFileArgs): Promise<string> => {
}: GetTerraformRootFileArgs): string => {
const nodeNames = nodes.map((entry) => entry.name);

const nodeCount = nodes.length;
Expand Down Expand Up @@ -86,12 +86,6 @@ const getTerraformRootFile = async ({
Deno.exit(1);
}

const tempFilePath = await Deno.makeTempFile();

// TODO: can we delete this?
Deno.writeTextFileSync(tempFilePath, googleCredentials); // contents of service account JSON written to temp file
Deno.env.set("GOOGLE_APPLICATION_CREDENTIALS", tempFilePath); // set env var to give terraform path to temp file

terraformDependencies.required_providers[0].google =
googleTerraformProviderDependency;

Expand Down
Loading

0 comments on commit 788724d

Please sign in to comment.