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

Commit a40b396

Browse files
[INFRA] Support for local paths Nate.infra.relative paths (#532)
* manage identity test docs * fixing tf module * code planning * init relative path support * updated parsing logic * additional changes * updated generated * add docs for local paths * updated changes * dennis updates * add unit tests * remove unwanted eslint disable and correct doc Co-authored-by: Dennis Seah <[email protected]>
1 parent 125c308 commit a40b396

File tree

4 files changed

+295
-2
lines changed

4 files changed

+295
-2
lines changed

guides/cloud-infra-management.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,81 @@ following:
5252
- **Using arguments** - Pass in your formatted source url for your private AzDO
5353
repo with the PAT and arbitrary username specified. Example
5454
`spk infra scaffold --name fabrikam --source https://spk:{$PAT}@dev.azure.com/microsoft/spk/_git/infra_repo --version master --template cluster/environments/azure-single-keyvault`
55+
56+
## Terraform Modules with Local Paths
57+
58+
`spk` now supports Terraform source templates that use a
59+
[local repository path](https://www.terraform.io/docs/modules/sources.html#local-paths)
60+
for references to modules. To obtain the modules for further teraform
61+
deployment, `spk infra generate` will shape a module source value from the
62+
`source`, `tempate`, and `version` arguments passed.
63+
64+
**Example:**
65+
66+
Template Main.tf
67+
68+
```tf
69+
"aks-gitops" {
70+
source = "../../azure/aks-gitops"
71+
acr_enabled = var.acr_enabled
72+
agent_vm_count = var.agent_vm_count
73+
agent_vm_size = var.agent_vm_size
74+
cluster_name = var.cluster_name
75+
dns_prefix = var.dns_prefix
76+
flux_recreate = var.flux_recreate
77+
gc_enabled = var.gc_enabled
78+
gitops_ssh_url = var.gitops_ssh_url
79+
gitops_ssh_key = var.gitops_ssh_key
80+
gitops_path = var.gitops_path
81+
gitops_poll_interval = var.gitops_poll_interval
82+
gitops_label = var.gitops_label
83+
gitops_url_branch = var.gitops_url_branch
84+
ssh_public_key = var.ssh_public_key
85+
resource_group_name = data.azurerm_resource_group.cluster_rg.name
86+
service_principal_id = var.service_principal_id
87+
service_principal_secret = var.service_principal_secret
88+
vnet_subnet_id = tostring(element(module.vnet.vnet_subnet_ids, 0))
89+
service_cidr = var.service_cidr
90+
dns_ip = var.dns_ip
91+
docker_cidr = var.docker_cidr
92+
network_plugin = var.network_plugin
93+
network_policy = var.network_policy
94+
oms_agent_enabled = var.oms_agent_enabled
95+
kubernetes_version = var.kubernetes_version
96+
}`;
97+
98+
```
99+
100+
SPK-generated Main.tf
101+
102+
```tf
103+
"aks-gitops" {
104+
source = "github.com/microsoft/bedrock?ref=master//cluster/azure/aks-gitops/"
105+
acr_enabled = var.acr_enabled
106+
agent_vm_count = var.agent_vm_count
107+
agent_vm_size = var.agent_vm_size
108+
cluster_name = var.cluster_name
109+
dns_prefix = var.dns_prefix
110+
flux_recreate = var.flux_recreate
111+
gc_enabled = var.gc_enabled
112+
gitops_ssh_url = var.gitops_ssh_url
113+
gitops_ssh_key = var.gitops_ssh_key
114+
gitops_path = var.gitops_path
115+
gitops_poll_interval = var.gitops_poll_interval
116+
gitops_label = var.gitops_label
117+
gitops_url_branch = var.gitops_url_branch
118+
ssh_public_key = var.ssh_public_key
119+
resource_group_name = data.azurerm_resource_group.cluster_rg.name
120+
service_principal_id = var.service_principal_id
121+
service_principal_secret = var.service_principal_secret
122+
vnet_subnet_id = tostring(element(module.vnet.vnet_subnet_ids, 0))
123+
service_cidr = var.service_cidr
124+
dns_ip = var.dns_ip
125+
docker_cidr = var.docker_cidr
126+
network_plugin = var.network_plugin
127+
network_policy = var.network_policy
128+
oms_agent_enabled = var.oms_agent_enabled
129+
kubernetes_version = var.kubernetes_version
130+
}`;
131+
132+
```

src/commands/infra/generate.test.ts

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import * as fsExtra from "fs-extra";
33
import path from "path";
44
import simpleGit from "simple-git/promise";
55
import { loadConfigurationFromLocalEnv, readYaml } from "../../config";
6+
import { getErrorMessage } from "../../lib/errorBuilder";
67
import { safeGitUrlForLogging } from "../../lib/gitutils";
7-
import { removeDir } from "../../lib/ioUtil";
8+
import { createTempDir, removeDir } from "../../lib/ioUtil";
89
import { disableVerboseLogging, enableVerboseLogging } from "../../logger";
910
import { InfraConfigYaml } from "../../types";
1011
import {
12+
checkModuleSource,
1113
checkRemoteGitExist,
1214
createGenerated,
1315
DefinitionYAMLExistence,
@@ -19,6 +21,8 @@ import {
1921
gitCheckout,
2022
gitClone,
2123
gitPull,
24+
inspectGeneratedSources,
25+
moduleSourceModify,
2226
retryRemoteValidate,
2327
validateDefinition,
2428
validateRemoteSource,
@@ -42,6 +46,25 @@ interface GitTestData {
4246
safeLoggingUrl: string;
4347
}
4448

49+
const mockTFData = `"aks-gitops" {
50+
source = "../../azure/aks-gitops"
51+
acr_enabled = var.acr_enabled
52+
agent_vm_count = var.agent_vm_count
53+
};`;
54+
55+
const mockSourceInfo = {
56+
source: "https://github.com/microsoft/bedrock.git",
57+
template: "cluster/environments/azure-single-keyvault",
58+
version: "v0.0.1",
59+
};
60+
61+
const modifedSourceModuleData = `"aks-gitops" {
62+
source = "github.com/microsoft/bedrock.git?ref=v0.0.1//cluster/azure/aks-gitops/"
63+
acr_enabled = var.acr_enabled
64+
agent_vm_count = var.agent_vm_count
65+
};
66+
`;
67+
4568
beforeAll(() => {
4669
enableVerboseLogging();
4770
});
@@ -778,3 +801,79 @@ describe("Validate backend.tfvars file", () => {
778801
);
779802
});
780803
});
804+
805+
describe("test checkModuleSource function", () => {
806+
it("positive test", () => {
807+
let res = checkModuleSource(`source="../../azure/aks-gitops"`);
808+
expect(res).toBeTruthy();
809+
res = checkModuleSource(`source='../../azure/aks-gitops'`);
810+
expect(res).toBeTruthy();
811+
res = checkModuleSource(`source= '../../azure/aks-gitops'`);
812+
expect(res).toBeTruthy();
813+
res = checkModuleSource(` source = '../../azure/aks-gitops'`);
814+
expect(res).toBeTruthy();
815+
res = checkModuleSource(` source ='../../azure/aks-gitops'`);
816+
expect(res).toBeTruthy();
817+
});
818+
it("negative test", () => {
819+
const res = checkModuleSource(`source="/azure/aks-gitops"`);
820+
expect(res).toBeFalsy();
821+
});
822+
});
823+
824+
describe("test moduleSourceModify function", () => {
825+
it("positive test", async () => {
826+
jest
827+
.spyOn(generate, "revparse")
828+
.mockResolvedValueOnce("cluster/azure/aks-gitops/");
829+
830+
const result = await moduleSourceModify(mockSourceInfo, mockTFData);
831+
expect(result).toBe(modifedSourceModuleData);
832+
});
833+
it("negative test", async () => {
834+
jest.spyOn(generate, "revparse").mockRejectedValueOnce(Error());
835+
836+
await expect(
837+
moduleSourceModify(mockSourceInfo, mockTFData)
838+
).rejects.toThrow(getErrorMessage("infra-module-source-modify-err"));
839+
});
840+
});
841+
842+
describe("test inspectGeneratedSources function", () => {
843+
it("positive test", async () => {
844+
const folderName = createTempDir();
845+
const fileName = path.join(folderName, "main.tf");
846+
fs.writeFileSync(fileName, mockTFData, "utf-8");
847+
jest
848+
.spyOn(generate, "moduleSourceModify")
849+
.mockResolvedValueOnce(modifedSourceModuleData);
850+
851+
await inspectGeneratedSources(folderName, mockSourceInfo);
852+
853+
const result = fs.readFileSync(fileName, "utf-8");
854+
expect(result).toBe(modifedSourceModuleData);
855+
});
856+
it("positive test: there are no files", async () => {
857+
const folderName = createTempDir();
858+
await inspectGeneratedSources(folderName, mockSourceInfo);
859+
});
860+
it("positive test: file content is not modified if it does not have .tf extension", async () => {
861+
const folderName = createTempDir();
862+
const fileName = path.join(folderName, "main.txt");
863+
fs.writeFileSync(fileName, mockTFData, "utf-8");
864+
865+
await inspectGeneratedSources(folderName, mockSourceInfo);
866+
const result = fs.readFileSync(fileName, "utf-8");
867+
expect(result).toBe(mockTFData);
868+
});
869+
it("negative test", async () => {
870+
const folderName = createTempDir();
871+
const fileName = path.join(folderName, "main.tf");
872+
fs.writeFileSync(fileName, mockTFData, "utf-8");
873+
jest.spyOn(generate, "moduleSourceModify").mockRejectedValueOnce(Error());
874+
875+
await expect(
876+
inspectGeneratedSources(folderName, mockSourceInfo)
877+
).rejects.toThrow(getErrorMessage("infra-inspect-generated-sources-err"));
878+
});
879+
});

src/commands/infra/generate.ts

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export enum DefinitionYAMLExistence {
4040
PARENT_ONLY,
4141
}
4242

43+
const regexSource = /^\s*source\s*=\s*["'](\.\.?\/[^"']*)["']$/gm;
44+
4345
/**
4446
* Checks if definition.yaml is present locally to provided project path
4547
*
@@ -501,6 +503,117 @@ export const singleDefinitionGeneration = async (
501503
await copyTfTemplate(templatePath, childDirectory, true);
502504
};
503505

506+
/**
507+
* Checks to see if module sources are local
508+
*
509+
* @param tfFile path to the terraform file in child directory
510+
*/
511+
export const checkModuleSource = (tfData: string): boolean => {
512+
// Check if the file string matches an instance of a module source value as a local path
513+
const matches = tfData.match(regexSource);
514+
return matches !== null;
515+
};
516+
517+
export const revparse = async (sPath: string): Promise<string> => {
518+
return await simpleGit(sPath).revparse(["--show-prefix"]);
519+
};
520+
521+
/**
522+
* Checks to see if module sources are local
523+
*
524+
* @param sourceConfig Array of source configuration
525+
*/
526+
export const moduleSourceModify = async (
527+
fileSource: SourceInformation,
528+
tfData: string
529+
): Promise<string> => {
530+
try {
531+
let result = "";
532+
const sourceFolder = getSourceFolderNameFromURL(fileSource.source);
533+
const sourcePath = path.join(spkTemplatesPath, sourceFolder);
534+
535+
// Split data by line and iterate
536+
for (let line of tfData.split(/\r?\n/)) {
537+
// Match line to expected module source format
538+
if (line.match(regexSource) !== null) {
539+
// Split the line into segments, the third element is the source value
540+
const splitLine = line.split(/\s+/);
541+
// Filter on module source value
542+
const moduleSource = new RegExp(
543+
splitLine[3].replace(/['"]+/g, ""),
544+
"g"
545+
);
546+
// Get relative path of terraform module local to the repo
547+
const repoModulePath = await revparse(
548+
path.join(
549+
sourcePath,
550+
fileSource.template,
551+
splitLine[3].replace(/["']/g, "")
552+
)
553+
);
554+
// Concatenate the Git URL with munged data
555+
const gitSource = fileSource.source
556+
.replace(/(^\w+:|^)\/\//g, "")
557+
.concat("?ref=", fileSource.version, "//", repoModulePath);
558+
// Replace the line
559+
line = line.replace(moduleSource, gitSource);
560+
}
561+
result += line + "\n";
562+
}
563+
return result;
564+
} catch (err) {
565+
throw buildError(
566+
errorStatusCode.EXE_FLOW_ERR,
567+
"infra-module-source-modify-err",
568+
err
569+
);
570+
}
571+
};
572+
573+
/**
574+
* Checks to see if module sources are local
575+
*
576+
* @param sourceConfig Array of source configuration
577+
*/
578+
export const inspectGeneratedSources = async (
579+
childDirectory: string,
580+
sourceConfig: SourceInformation
581+
): Promise<void> => {
582+
try {
583+
// Support for local source paths, check template directory .tf files to generate git paths for terraform modules
584+
const files = fsExtra.readdirSync(childDirectory, "utf-8");
585+
for (const file of files) {
586+
if (path.extname(file) === ".tf") {
587+
const tfData = fsExtra.readFileSync(
588+
path.join(childDirectory, file),
589+
"utf8"
590+
);
591+
const containsLocalSource = checkModuleSource(tfData);
592+
if (containsLocalSource) {
593+
logger.info(
594+
`Local relative paths for module source values detected in terraform file: ${file}`
595+
);
596+
const mungeData = await moduleSourceModify(sourceConfig, tfData);
597+
logger.info(
598+
`Terraform File: ${file} local module source values successfully converted to git source paths`
599+
);
600+
fsExtra.writeFileSync(
601+
path.join(childDirectory, file),
602+
mungeData,
603+
"utf8"
604+
);
605+
}
606+
}
607+
}
608+
} catch (err) {
609+
throw buildError(
610+
errorStatusCode.EXE_FLOW_ERR,
611+
"infra-inspect-generated-sources-err",
612+
err
613+
);
614+
}
615+
};
616+
504617
/**
505618
* Creates "generated" directory if it does not already exists
506619
*
@@ -543,7 +656,6 @@ export const generateConfig = async (
543656
createGenerated(parentDirectory);
544657
createGenerated(childDirectory);
545658
}
546-
547659
combineVariable(
548660
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
549661
parentInfraConfig.variables!,
@@ -576,6 +688,8 @@ export const generateConfig = async (
576688
templatePath
577689
);
578690
}
691+
// Modify generated TF files if it contains local sources
692+
await inspectGeneratedSources(childDirectory, sourceConfig);
579693
};
580694

581695
export const execute = async (

src/lib/i18n.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
"infra-scaffold-cmd-values-missing": "Values for name, version and/or 'template were missing. Provide value for values for them.",
5959

6060
"infra-generate-cmd-failed": "Infra generate command was not successfully executed.",
61+
"infra-module-source-modify-err": "Could not modify source module.",
62+
"infra-inspect-generated-sources-err": "Could not generated sources.",
6163

6264
"infra-defn-yaml-not-found": "{0} was not found in {1}",
6365
"infra-defn-yaml-invalid": "The {0} file is invalid. There are missing fields. template: {1} source: {2} version: {3}.",

0 commit comments

Comments
 (0)