Skip to content
This repository was archived by the owner on Apr 13, 2020. It is now read-only.

Commit 86306ee

Browse files
authored
spk infra generate (iteration 2) (#132)
* Inheritance functionality * Fixed regex bug * Removed `fs` operations from unit tests * Modularized generate.ts * Copy exclude `terraform.tfvars`
1 parent d1d5f80 commit 86306ee

File tree

5 files changed

+204
-40
lines changed

5 files changed

+204
-40
lines changed

docs/cloud-infra-management.md

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ Generates a deployment folder of an infrastructure scaffolded project containing
141141
a `definition.json` that contains a `source`, `template` and `version` to obtain
142142
and complete the terraform template files.
143143

144-
It will do the following (**In Progress**):
144+
It will do the following:
145145

146146
- Check if a provided project folder contains a `definition.json`
147147
- Check if the terraform template `source` provided has a valid remote
@@ -156,6 +156,82 @@ It will do the following (**In Progress**):
156156
- Create a `spk.tfvars` in the generated directory based on the variables
157157
provided in `definition.json`
158158

159+
```
160+
Usage:
161+
spk infra generate|g [options]
162+
163+
Generate scaffold for terraform cluster deployment.
164+
165+
Options:
166+
-p, --project <path to project folder to generate> Location of the definition.json file that will be generated
167+
-h, --help output usage information
168+
```
169+
170+
### generate example
171+
172+
Assuming you have the following setup:
173+
174+
```
175+
discovery-service
176+
|- definition.json
177+
|- east/
178+
|- definition.json
179+
|- central/
180+
|- definition.json
181+
```
182+
183+
When executing the following command **in the `discovery-service` directory**:
184+
185+
```
186+
spk infra generate --project east
187+
```
188+
189+
The following hiearchy of directories will be generated _alongside_ the targeted
190+
directory. In addition, the appropriate versioned Terraform templates will be
191+
copied over to the leaf directory with a `spk.tfvars`, which contains the
192+
variables accumulated from parent **and** leaf definition.json files.
193+
194+
```
195+
discovery-service
196+
|- definition.json
197+
|- east/
198+
|- definition.json
199+
|- central/
200+
|- definition.json
201+
discovery-service-generated
202+
|- east
203+
|- main.tf
204+
|- variables.tf
205+
|- spk.tfvars
206+
```
207+
208+
You can also have a "single-tree" generation by executing `spk infra generate`
209+
at the level above the targeted directory. For example, if you had the following
210+
tree structure:
211+
212+
```
213+
discovery-service
214+
|- east/
215+
|- definition.json
216+
|- central/
217+
|- definition.json
218+
```
219+
220+
and wanted to create _just_ an `east-generated` directory, you could run
221+
`spk infra generate -p east`, and this will result in the following:
222+
223+
```
224+
discovery-service
225+
|- east/
226+
|- definition.json
227+
|- east-generated/
228+
|- main.tf
229+
|- variables.tf
230+
|- spk.tfvars
231+
|- central/
232+
|- definition.json
233+
```
234+
159235
### Authentication
160236

161237
Spk currently supports the use of Personal Access Tokens to authenticate with
@@ -168,3 +244,4 @@ build scaffolded definitions using a private AzDO repo, do one of the following:
168244
- **Using arguments** - Pass in your formatted source url for your private AzDO
169245
repo with the PAT and arbitrary username specified. Example
170246
`spk infra scaffold --name discovery-service --source https://spk:{my_PAT_Token}@dev.azure.com/microsoft/spk/_git/infra_repo --version v0.0.1 --template cluster/environments/azure-single-keyvault`
247+

docs/infra/spk-terragrunt-day-2-scenarios.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,4 +379,4 @@ Additional Thoughts:
379379

380380
## Holder for comparison between Terragrunt & Bespoke
381381

382-
![](../images/Capture.PNG)
382+
![](../images/Capture.png)

src/commands/infra/generate.test.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import {
99
generateSpkTfvars,
1010
parseDefinitionJson,
11+
readDefinitionJson,
1112
validateDefinition,
1213
validateRemoteSource,
1314
validateTemplateSource
@@ -83,15 +84,8 @@ describe("Validate template path from a definition.json", () => {
8384
describe("Validate spk.tfvars file", () => {
8485
test("Validating that a spk.tfvars is generated and has appropriate format", async () => {
8586
const mockProjectPath = "src/commands/infra/mocks";
86-
const generateTfvars = await generateSpkTfvars(
87-
mockProjectPath,
88-
mockProjectPath
89-
);
90-
const data = fs.readFileSync(
91-
path.join(mockProjectPath, "spk.tfvars"),
92-
"utf-8"
93-
);
94-
expect(data).toContain('gitops_poll_interval = "5m"');
95-
fs.unlinkSync(path.join(mockProjectPath, "spk.tfvars"));
87+
const definitionJSON = await readDefinitionJson(mockProjectPath);
88+
const spkTfvarsObject = await generateSpkTfvars(definitionJSON);
89+
expect(spkTfvarsObject).toContain('gitops_poll_interval = "5m"');
9690
});
9791
});

src/commands/infra/generate.ts

Lines changed: 115 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,7 @@ export const generateCommandDecorator = (command: commander.Command): void => {
3939
await validateDefinition(opts.project);
4040
const jsonSource = await validateTemplateSource(opts.project);
4141
await validateRemoteSource(jsonSource);
42-
const generatedDir = await createGenerated(opts.project);
43-
const templatePath = await parseDefinitionJson(opts.project);
44-
await copyTfTemplate(templatePath, generatedDir);
45-
await generateSpkTfvars(opts.project, generatedDir);
42+
await generateConfig(opts.project);
4643
} catch (err) {
4744
logger.error(
4845
"Error occurred while generating project deployment files"
@@ -175,14 +172,82 @@ export const validateRemoteSource = async (
175172
return true;
176173
};
177174

175+
/**
176+
* Creates "generated" directory if it does not already exists
177+
*
178+
* @param projectPath Path to the definition.json file
179+
*/
180+
export const generateConfig = async (projectPath: string): Promise<void> => {
181+
try {
182+
// First, search for definition.json in current working directory
183+
const templatePath = await parseDefinitionJson(projectPath);
184+
const cwdPath = process.cwd();
185+
if (fs.existsSync(path.join(cwdPath, "definition.json"))) {
186+
// If there exists a definition.json, then read file
187+
logger.info(`A definition.json was found in the parent directory.`);
188+
const parentDefinitionJSON = await readDefinitionJson(cwdPath);
189+
const leafDefinitionJSON = await readDefinitionJson(projectPath);
190+
/* Iterate through parent and leaf JSON objects to find matches
191+
If there is a match, then replace parent key-value
192+
If there is no match between the parent and leaf,
193+
then append leaf key-value parent key-value JSON */
194+
for (const parentKey in parentDefinitionJSON.variables) {
195+
if (parentKey) {
196+
for (const leafKey in leafDefinitionJSON.variables) {
197+
if (parentKey === leafKey) {
198+
let parentVal = parentDefinitionJSON.variables[parentKey];
199+
parentVal = leafDefinitionJSON.variables[leafKey];
200+
} else {
201+
// Append to parent variables block
202+
const leafVal = leafDefinitionJSON.variables[leafKey];
203+
parentDefinitionJSON.variables[leafKey] = leafVal;
204+
}
205+
}
206+
}
207+
}
208+
// Create a generated parent directory
209+
const parentDirectory = await createGenerated(cwdPath + "-generated");
210+
// Then, create generated child directory
211+
const childDirectory = await createGenerated(
212+
path.join(parentDirectory, projectPath)
213+
);
214+
// Generate Terraform files in generated directory
215+
const spkTfvarsObject = await generateSpkTfvars(
216+
parentDefinitionJSON.variables
217+
);
218+
await checkSpkTfvars(childDirectory);
219+
await writeToSpkTfvarsFile(spkTfvarsObject, childDirectory);
220+
// const templatePath = await parseDefinitionJson(projectPath);
221+
await copyTfTemplate(templatePath, childDirectory);
222+
} else {
223+
// If there is not a definition.json in current working directory,
224+
// then proceed with reading definition.json in project path
225+
// await createGenerated(projectPath)
226+
// logger.info(`A definition.json was not found in the parent directory.`)
227+
const definitionJSON = await readDefinitionJson(projectPath);
228+
// Create a generated directory
229+
const generatedDirectory = await createGenerated(
230+
projectPath + "-generated"
231+
);
232+
// Generate Terraform files in generated directory
233+
const spkTfvarsObject = await generateSpkTfvars(definitionJSON.variables);
234+
await checkSpkTfvars(generatedDirectory);
235+
await writeToSpkTfvarsFile(spkTfvarsObject, generatedDirectory);
236+
await copyTfTemplate(templatePath, generatedDirectory);
237+
}
238+
} catch (err) {
239+
return err;
240+
}
241+
};
242+
178243
/**
179244
* Creates "generated" directory if it does not already exists
180245
*
181246
* @param projectPath Path to the definition.json file
182247
*/
183248
export const createGenerated = async (projectPath: string): Promise<string> => {
184249
try {
185-
const newGeneratedPath = projectPath + "-generated";
250+
const newGeneratedPath = projectPath;
186251
mkdirp.sync(newGeneratedPath);
187252
logger.info(`Created generated directory: ${newGeneratedPath}`);
188253
return newGeneratedPath;
@@ -210,6 +275,22 @@ export const parseDefinitionJson = async (projectPath: string) => {
210275
return templatePath;
211276
};
212277

278+
/**
279+
* Checks if an spk.tfvars
280+
*
281+
* @param projectPath Path to the spk.tfvars file
282+
*/
283+
export const checkSpkTfvars = async (generatedPath: string): Promise<void> => {
284+
try {
285+
// Remove existing spk.tfvars if it already exists
286+
if (fs.existsSync(path.join(generatedPath, "spk.tfvars"))) {
287+
fs.unlinkSync(path.join(generatedPath, "spk.tfvars"));
288+
}
289+
} catch (err) {
290+
return err;
291+
}
292+
};
293+
213294
/**
214295
*
215296
* Takes in the "variables" block from definition.json file and returns
@@ -224,41 +305,49 @@ export const parseDefinitionJson = async (projectPath: string) => {
224305
* key = "value"
225306
*
226307
*/
227-
export const generateSpkTfvars = async (
228-
projectPath: string,
229-
generatedPath: string
230-
) => {
308+
export const generateSpkTfvars = async (definitionJSON: string[]) => {
231309
try {
232-
// Remove existing spk.tfvars if it already exists
233-
if (fs.existsSync(path.join(generatedPath, "spk.tfvars"))) {
234-
fs.unlinkSync(path.join(generatedPath, "spk.tfvars"));
235-
}
236-
// Parse definition.json and extract "variables"
237-
const definitionJSON = await readDefinitionJson(projectPath);
238-
const variables = definitionJSON.variables;
310+
const tfVars: string[] = [];
311+
// Parse definition.json "variables" block
312+
const variables = definitionJSON;
239313
// Restructure the format of variables text
240314
const tfVariables = JSON.stringify(variables)
241-
.replace(/\:/g, "=")
242315
.replace(/\{|\}/g, "")
243316
.replace(/\,/g, "\n")
244317
.split("\n");
245318
// (re)Create spk.tfvars
246319
tfVariables.forEach(t => {
247-
const tfVar = t.split("=");
248-
if (tfVar[0].length > 0) {
249-
tfVar[0] = tfVar[0].replace(/\"/g, "");
320+
const tfVar = t.split(":");
321+
const result = tfVar.slice(0, 1);
322+
result.push(tfVar.slice(1).join(":"));
323+
if (result[0].length > 0) {
324+
result[0] = result[0].replace(/\"/g, "");
250325
}
251-
const newTfVar = tfVar.join(" = ");
252-
fs.appendFileSync(
253-
path.join(generatedPath, "spk.tfvars"),
254-
newTfVar + "\n"
255-
);
326+
const newTfVar = result.join(" = ");
327+
tfVars.push(newTfVar);
256328
});
329+
return tfVars;
257330
} catch (err) {
258-
logger.error(err);
331+
logger.error(`There was an error with generating the spk tfvars object.`);
332+
return err;
259333
}
260334
};
261335

336+
/**
337+
* Reads in a tfVars object and returns a spk.tfvars file
338+
*
339+
* @param spkTfVars spk tfvars object in an array
340+
* @param generatedPath Path to write the spk.tfvars file to
341+
*/
342+
export const writeToSpkTfvarsFile = async (
343+
spkTfVars: string[],
344+
generatedPath: string
345+
) => {
346+
spkTfVars.forEach(tfvar => {
347+
fs.appendFileSync(path.join(generatedPath, "spk.tfvars"), tfvar + "\n");
348+
});
349+
};
350+
262351
/**
263352
* Reads a definition.json and returns a JSON object
264353
*

src/commands/infra/scaffold.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export const renameTfvars = async (dir: string): Promise<void> => {
135135
try {
136136
const tfFiles = fs.readdirSync(dir);
137137
tfFiles.forEach(file => {
138-
if (file.substr(file.lastIndexOf(".") + 1) === "tfvars") {
138+
if (file === "terraform.tfvars") {
139139
fs.renameSync(path.join(dir, file), path.join(dir, file + ".backup"));
140140
}
141141
});
@@ -156,7 +156,11 @@ export const copyTfTemplate = async (
156156
envName: string
157157
): Promise<boolean> => {
158158
try {
159-
await fsextra.copy(templatePath, envName);
159+
await fsextra.copy(templatePath, envName, {
160+
filter: file => {
161+
return !(file.indexOf("terraform.tfvars") > -1);
162+
}
163+
});
160164
logger.info(`Terraform template files copied from ${templatePath}`);
161165
} catch (err) {
162166
logger.error(

0 commit comments

Comments
 (0)