tfv 4.0.4 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +15 -8
- package/README.md +73 -5
- package/lib/commands/apply.js +22 -0
- package/lib/commands/destroy.js +22 -0
- package/lib/commands/install.js +10 -7
- package/lib/commands/list.js +16 -13
- package/lib/commands/plan.js +22 -0
- package/lib/commands/remove.js +8 -6
- package/lib/commands/switch.js +8 -6
- package/lib/commands/use.js +9 -6
- package/lib/modules/list.js +84 -6
- package/lib/modules/terraform-command.js +162 -0
- package/package.json +5 -1
|
@@ -4,18 +4,25 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
branches: [main]
|
|
6
6
|
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
id-token: write
|
|
10
|
+
|
|
7
11
|
jobs:
|
|
8
12
|
publish:
|
|
9
13
|
runs-on: ubuntu-latest
|
|
14
|
+
|
|
10
15
|
steps:
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
uses: actions/setup-node@v1
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
15
19
|
with:
|
|
16
|
-
node-version:
|
|
20
|
+
node-version: 18
|
|
17
21
|
registry-url: https://registry.npmjs.org
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
cache: 'npm'
|
|
23
|
+
|
|
24
|
+
- run: npm ci
|
|
25
|
+
|
|
26
|
+
- run: npm publish --access public
|
|
20
27
|
env:
|
|
21
|
-
|
|
28
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
CHANGED
|
@@ -42,11 +42,28 @@ tfv -h
|
|
|
42
42
|
tfv <command>
|
|
43
43
|
|
|
44
44
|
Commands:
|
|
45
|
-
|
|
46
|
-
tfv
|
|
47
|
-
|
|
48
|
-
tfv
|
|
49
|
-
|
|
45
|
+
|
|
46
|
+
tfv install <version> [option] Install a terraform version [aliases: i]
|
|
47
|
+
|
|
48
|
+
tfv list [option] List installed or available terraform versions [aliases: ls]
|
|
49
|
+
|
|
50
|
+
tfv remove <version> Remove terraform versions from tfv store [aliases: rm]
|
|
51
|
+
|
|
52
|
+
tfv auto-switch Auto-detect and switch to your project terraform version [aliases: as]
|
|
53
|
+
|
|
54
|
+
tfv use <version> Switch to a specified terraform version
|
|
55
|
+
|
|
56
|
+
tfv apply Run terraform apply with optional file-based targets.
|
|
57
|
+
Accepts all terraform flags after --
|
|
58
|
+
Example: tfv apply --file main.tf --file network.tf -- -auto-approve -target=<TARGET> -var="env=prod"
|
|
59
|
+
|
|
60
|
+
tfv destroy Run terraform destroy with optional file-based targets.
|
|
61
|
+
Accepts all terraform flags after --
|
|
62
|
+
Example: tfv destroy --file main.tf --file network.tf -- -auto-approve -target=<TARGET> -var="env=prod"
|
|
63
|
+
|
|
64
|
+
tfv plan Run terraform plan with optional file-based targets.
|
|
65
|
+
Accepts all terraform flags after --
|
|
66
|
+
Example: tfv plan --file main.tf --file network.tf -- -auto-approve -target=<TARGET> -var="env=prod"
|
|
50
67
|
|
|
51
68
|
Options:
|
|
52
69
|
-h, --help Show help [boolean]
|
|
@@ -67,6 +84,9 @@ https://github.com/marcdomain/tfv/assets/25563661/fa44f0f2-2dca-4f22-9fea-c74e4b
|
|
|
67
84
|
* [list](#list)
|
|
68
85
|
* [remove](#remove)
|
|
69
86
|
* [auto-switch](#auto-switch)
|
|
87
|
+
* [plan](#plan)
|
|
88
|
+
* [apply](#apply)
|
|
89
|
+
* [destroy](#destroy)
|
|
70
90
|
<!--te-->
|
|
71
91
|
|
|
72
92
|
### Modules
|
|
@@ -162,3 +182,51 @@ Run with alias
|
|
|
162
182
|
```sh
|
|
163
183
|
tfv as
|
|
164
184
|
```
|
|
185
|
+
|
|
186
|
+
- #### _PLAN_
|
|
187
|
+
|
|
188
|
+
Run terraform plan with optional file-based targets. Parses terraform files to extract resources, data sources, and modules as targets.
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
tfv plan --file main.tf
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
With multiple files
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
tfv plan --file main.tf --file network.tf
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
With extra terraform flags
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
tfv plan --file main.tf -- -var="env=prod" -out=plan.out
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- #### _APPLY_
|
|
207
|
+
|
|
208
|
+
Run terraform apply with optional file-based targets.
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
tfv apply --file main.tf
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
With auto-approve
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
tfv apply --file main.tf -- -auto-approve
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
- #### _DESTROY_
|
|
221
|
+
|
|
222
|
+
Run terraform destroy with optional file-based targets.
|
|
223
|
+
|
|
224
|
+
```sh
|
|
225
|
+
tfv destroy --file main.tf
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
With auto-approve
|
|
229
|
+
|
|
230
|
+
```sh
|
|
231
|
+
tfv destroy --file main.tf -- -auto-approve
|
|
232
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {runTerraformCommand} = require('../modules/terraform-command');
|
|
4
|
+
|
|
5
|
+
exports.command = 'apply'
|
|
6
|
+
exports.desc = 'Run terraform apply with optional file-based targets.\n'
|
|
7
|
+
exports.builder = (yargs) => {
|
|
8
|
+
return yargs
|
|
9
|
+
.option('file', {
|
|
10
|
+
alias: 'f',
|
|
11
|
+
describe: 'Terraform file(s) to extract targets from',
|
|
12
|
+
type: 'array',
|
|
13
|
+
})
|
|
14
|
+
.epilog('Accepts all terraform flags after --\nExample: tfv apply --file main.tf --file network.tf -- -auto-approve -target=<TARGET> -var="env=prod"')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
exports.handler = async (argv) => {
|
|
18
|
+
const {file, _} = argv;
|
|
19
|
+
// Extra args come after -- in the command line
|
|
20
|
+
const extraArgs = _.slice(1);
|
|
21
|
+
await runTerraformCommand('apply', file, extraArgs);
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {runTerraformCommand} = require('../modules/terraform-command');
|
|
4
|
+
|
|
5
|
+
exports.command = 'destroy'
|
|
6
|
+
exports.desc = 'Run terraform destroy with optional file-based targets.\n'
|
|
7
|
+
exports.builder = (yargs) => {
|
|
8
|
+
return yargs
|
|
9
|
+
.option('file', {
|
|
10
|
+
alias: 'f',
|
|
11
|
+
describe: 'Terraform file(s) to extract targets from',
|
|
12
|
+
type: 'array',
|
|
13
|
+
})
|
|
14
|
+
.epilog('Accepts all terraform flags after --\nExample: tfv destroy --file main.tf --file network.tf -- -auto-approve -target=<TARGET> -var="env=prod"')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
exports.handler = async (argv) => {
|
|
18
|
+
const {file, _} = argv;
|
|
19
|
+
// Extra args come after -- in the command line
|
|
20
|
+
const extraArgs = _.slice(1);
|
|
21
|
+
await runTerraformCommand('destroy', file, extraArgs);
|
|
22
|
+
}
|
package/lib/commands/install.js
CHANGED
|
@@ -6,13 +6,15 @@ const {install} = require('../modules/install');
|
|
|
6
6
|
exports.command = 'install <version> [option]'
|
|
7
7
|
exports.aliases = ['i']
|
|
8
8
|
exports.desc = 'Install a terraform version'
|
|
9
|
-
exports.builder = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
exports.builder = (yargs) => {
|
|
10
|
+
return yargs
|
|
11
|
+
.option('arch', {
|
|
12
|
+
alias: 'a',
|
|
13
|
+
describe: 'Specify system architecture. Defaults is the user system architecture',
|
|
14
|
+
type: 'string',
|
|
15
|
+
default: ''
|
|
16
|
+
})
|
|
17
|
+
.epilog('Version formats: latest, x.x.x (exact), x^ (latest major), x.x.^ (latest minor)\nExample: tfv install 1.5.7 --arch amd64')
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
exports.handler = async () => {
|
|
@@ -21,3 +23,4 @@ exports.handler = async () => {
|
|
|
21
23
|
|
|
22
24
|
await install(version, arch);
|
|
23
25
|
}
|
|
26
|
+
|
package/lib/commands/list.js
CHANGED
|
@@ -5,22 +5,25 @@ const {list} = require('../modules/list');
|
|
|
5
5
|
|
|
6
6
|
exports.command = 'list [option]'
|
|
7
7
|
exports.aliases = ['ls']
|
|
8
|
-
exports.desc = '
|
|
9
|
-
exports.builder = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
exports.desc = 'List installed or available terraform versions'
|
|
9
|
+
exports.builder = (yargs) => {
|
|
10
|
+
return yargs
|
|
11
|
+
.option('local', {
|
|
12
|
+
alias: 'l',
|
|
13
|
+
describe: 'List terraform versions you have installed using tfv',
|
|
14
|
+
type: 'boolean',
|
|
15
|
+
default: true,
|
|
16
|
+
})
|
|
17
|
+
.option('remote', {
|
|
18
|
+
alias: 'r',
|
|
19
|
+
describe: 'List all available terraform versions',
|
|
20
|
+
type: 'boolean',
|
|
21
|
+
})
|
|
22
|
+
.epilog('Examples:\n tfv ls List locally installed versions\n tfv ls --remote List all available remote versions')
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
exports.handler = async () => {
|
|
24
26
|
const {local, remote} = yargs.argv;
|
|
25
27
|
await list(local, remote);
|
|
26
28
|
}
|
|
29
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {runTerraformCommand} = require('../modules/terraform-command');
|
|
4
|
+
|
|
5
|
+
exports.command = 'plan'
|
|
6
|
+
exports.desc = 'Run terraform plan with optional file-based targets.\n'
|
|
7
|
+
exports.builder = (yargs) => {
|
|
8
|
+
return yargs
|
|
9
|
+
.option('file', {
|
|
10
|
+
alias: 'f',
|
|
11
|
+
describe: 'Terraform file(s) to extract targets from',
|
|
12
|
+
type: 'array',
|
|
13
|
+
})
|
|
14
|
+
.epilog('Accepts all terraform flags after --\nExample: tfv plan --file main.tf --file network.tf -- -auto-approve -target=<TARGET> -var="env=prod"')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
exports.handler = async (argv) => {
|
|
18
|
+
const {file, _} = argv;
|
|
19
|
+
// Extra args come after -- in the command line
|
|
20
|
+
const extraArgs = _.slice(1);
|
|
21
|
+
await runTerraformCommand('plan', file, extraArgs);
|
|
22
|
+
}
|
package/lib/commands/remove.js
CHANGED
|
@@ -5,12 +5,14 @@ const {remove} = require('../modules/remove');
|
|
|
5
5
|
|
|
6
6
|
exports.command = 'remove <version>'
|
|
7
7
|
exports.aliases = ['rm']
|
|
8
|
-
exports.desc = 'Remove
|
|
9
|
-
exports.builder = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
exports.desc = 'Remove terraform versions from tfv store'
|
|
9
|
+
exports.builder = (yargs) => {
|
|
10
|
+
return yargs
|
|
11
|
+
.option('verbose', {
|
|
12
|
+
describe: 'Produce detailed output',
|
|
13
|
+
type: 'boolean',
|
|
14
|
+
})
|
|
15
|
+
.epilog('Example: tfv rm 1.5.7 1.4.6')
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
exports.handler = async () => {
|
package/lib/commands/switch.js
CHANGED
|
@@ -4,12 +4,14 @@ const {autoSwitch} = require('../modules/switch');
|
|
|
4
4
|
|
|
5
5
|
exports.command = 'auto-switch'
|
|
6
6
|
exports.aliases = ['as']
|
|
7
|
-
exports.desc = '
|
|
8
|
-
exports.builder = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
exports.desc = 'Auto-detect and switch to your project terraform version'
|
|
8
|
+
exports.builder = (yargs) => {
|
|
9
|
+
return yargs
|
|
10
|
+
.option('verbose', {
|
|
11
|
+
describe: 'Produce detailed output',
|
|
12
|
+
type: 'boolean'
|
|
13
|
+
})
|
|
14
|
+
.epilog('Reads version from .terraform-version or required_version in .tf files\nExample: tfv auto-switch')
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
exports.handler = async () => {
|
package/lib/commands/use.js
CHANGED
|
@@ -4,12 +4,14 @@ const yargs = require('yargs');
|
|
|
4
4
|
const {use} = require('../modules/use');
|
|
5
5
|
|
|
6
6
|
exports.command = 'use <version>'
|
|
7
|
-
exports.desc = '
|
|
8
|
-
exports.builder = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
exports.desc = 'Switch to a specified terraform version\n'
|
|
8
|
+
exports.builder = (yargs) => {
|
|
9
|
+
return yargs
|
|
10
|
+
.option('verbose', {
|
|
11
|
+
describe: 'Produce detailed output',
|
|
12
|
+
type: 'boolean',
|
|
13
|
+
})
|
|
14
|
+
.epilog('Example: tfv use 1.5.7')
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
exports.handler = async () => {
|
|
@@ -17,3 +19,4 @@ exports.handler = async () => {
|
|
|
17
19
|
|
|
18
20
|
await use(version);
|
|
19
21
|
}
|
|
22
|
+
|
package/lib/modules/list.js
CHANGED
|
@@ -6,20 +6,98 @@ const {formatVersions} = require('../utils/formatVersions');
|
|
|
6
6
|
const {P_END, P_OK} = require('../utils/colors');
|
|
7
7
|
const {checkStore} = require('../utils/store');
|
|
8
8
|
|
|
9
|
+
const MAX_COLUMNS_PER_TABLE = 6;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Groups versions by their major.minor prefix
|
|
13
|
+
*/
|
|
14
|
+
const groupVersionsByRelease = (versions) => {
|
|
15
|
+
const groups = {};
|
|
16
|
+
|
|
17
|
+
versions.forEach(version => {
|
|
18
|
+
const parts = version.split('.');
|
|
19
|
+
if (parts.length >= 2) {
|
|
20
|
+
const majorMinor = `${parts[0]}.${parts[1]}`;
|
|
21
|
+
if (!groups[majorMinor]) {
|
|
22
|
+
groups[majorMinor] = [];
|
|
23
|
+
}
|
|
24
|
+
groups[majorMinor].push(version);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return groups;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Displays versions in table format with first version of each release as column header
|
|
33
|
+
* Breaks into multiple tables if columns exceed MAX_COLUMNS_PER_TABLE
|
|
34
|
+
*/
|
|
35
|
+
const displayVersionTable = (versions) => {
|
|
36
|
+
const groups = groupVersionsByRelease(versions);
|
|
37
|
+
|
|
38
|
+
// Sort release groups by version (descending)
|
|
39
|
+
const sortedKeys = Object.keys(groups).sort((a, b) => {
|
|
40
|
+
const [aMajor, aMinor] = a.split('.').map(Number);
|
|
41
|
+
const [bMajor, bMinor] = b.split('.').map(Number);
|
|
42
|
+
if (bMajor !== aMajor) return bMajor - aMajor;
|
|
43
|
+
return bMinor - aMinor;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Sort versions within each group (descending)
|
|
47
|
+
sortedKeys.forEach(key => {
|
|
48
|
+
groups[key].sort((a, b) => {
|
|
49
|
+
const aParts = a.split('.').map(n => parseInt(n) || 0);
|
|
50
|
+
const bParts = b.split('.').map(n => parseInt(n) || 0);
|
|
51
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
52
|
+
const aVal = aParts[i] || 0;
|
|
53
|
+
const bVal = bParts[i] || 0;
|
|
54
|
+
if (bVal !== aVal) return bVal - aVal;
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Split into chunks of MAX_COLUMNS_PER_TABLE
|
|
61
|
+
const chunks = [];
|
|
62
|
+
for (let i = 0; i < sortedKeys.length; i += MAX_COLUMNS_PER_TABLE) {
|
|
63
|
+
chunks.push(sortedKeys.slice(i, i + MAX_COLUMNS_PER_TABLE));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Display each chunk as a separate table
|
|
67
|
+
chunks.forEach((chunkKeys, chunkIndex) => {
|
|
68
|
+
// Find max rows needed for this chunk
|
|
69
|
+
const maxRows = Math.max(...chunkKeys.map(key => groups[key].length));
|
|
70
|
+
|
|
71
|
+
// Build table data
|
|
72
|
+
const tableData = [];
|
|
73
|
+
for (let row = 0; row < maxRows; row++) {
|
|
74
|
+
const rowObj = {};
|
|
75
|
+
chunkKeys.forEach(key => {
|
|
76
|
+
// Use major.minor.x format as header (e.g., "1.5.x")
|
|
77
|
+
const header = `${key}.x`;
|
|
78
|
+
const value = groups[key][row] || '';
|
|
79
|
+
rowObj[header] = value;
|
|
80
|
+
});
|
|
81
|
+
tableData.push(rowObj);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (chunks.length > 1) {
|
|
85
|
+
console.log(`\n${P_OK}Table ${chunkIndex + 1} of ${chunks.length}${P_END}`);
|
|
86
|
+
}
|
|
87
|
+
console.table(tableData);
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
9
91
|
exports.list = async (local, remote) => {
|
|
10
92
|
try {
|
|
11
93
|
let versions;
|
|
12
94
|
|
|
13
|
-
const result = (message, v) => {
|
|
14
|
-
console.log(`${P_OK}${message}${P_END}` + '\n');
|
|
15
|
-
console.log(v.join('\n'));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
95
|
if (remote) {
|
|
19
96
|
const data = await fetchAllVersions();
|
|
20
97
|
versions = formatVersions(data);
|
|
21
98
|
|
|
22
|
-
|
|
99
|
+
console.log(`${P_OK}List of all available terraform versions${P_END}\n`);
|
|
100
|
+
return displayVersionTable(versions);
|
|
23
101
|
}
|
|
24
102
|
|
|
25
103
|
if (local) {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const {spawn} = require('child_process');
|
|
3
|
+
const {P_END, P_OK, P_ERROR, P_WARN, P_INFO} = require('../utils/colors');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Removes comments from Terraform content
|
|
7
|
+
* Handles: # comments, // comments, and block comments
|
|
8
|
+
*/
|
|
9
|
+
const removeComments = (content) => {
|
|
10
|
+
// Remove block comments /* */
|
|
11
|
+
content = content.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
12
|
+
|
|
13
|
+
// Remove single line comments (# and //)
|
|
14
|
+
const lines = content.split('\n');
|
|
15
|
+
const cleanedLines = lines.map(line => {
|
|
16
|
+
// Find position of # or // that's not inside a string
|
|
17
|
+
let inString = false;
|
|
18
|
+
let stringChar = '';
|
|
19
|
+
let commentStart = -1;
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < line.length; i++) {
|
|
22
|
+
const char = line[i];
|
|
23
|
+
const nextChar = line[i + 1];
|
|
24
|
+
|
|
25
|
+
if (!inString && (char === '"' || char === "'")) {
|
|
26
|
+
inString = true;
|
|
27
|
+
stringChar = char;
|
|
28
|
+
} else if (inString && char === stringChar && line[i - 1] !== '\\') {
|
|
29
|
+
inString = false;
|
|
30
|
+
} else if (!inString) {
|
|
31
|
+
if (char === '#' || (char === '/' && nextChar === '/')) {
|
|
32
|
+
commentStart = i;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (commentStart !== -1) {
|
|
39
|
+
return line.substring(0, commentStart);
|
|
40
|
+
}
|
|
41
|
+
return line;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return cleanedLines.join('\n');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extracts terraform targets from file content
|
|
49
|
+
* Returns array of target strings like "aws_instance.example", "module.vpc", etc.
|
|
50
|
+
*/
|
|
51
|
+
const extractTargets = (content) => {
|
|
52
|
+
const targets = [];
|
|
53
|
+
|
|
54
|
+
// Remove comments first
|
|
55
|
+
const cleanContent = removeComments(content);
|
|
56
|
+
|
|
57
|
+
// Pattern for resource blocks: resource "type" "name"
|
|
58
|
+
const resourcePattern = /resource\s+"([^"]+)"\s+"([^"]+)"/g;
|
|
59
|
+
let match;
|
|
60
|
+
while ((match = resourcePattern.exec(cleanContent)) !== null) {
|
|
61
|
+
targets.push(`${match[1]}.${match[2]}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Pattern for data blocks: data "type" "name"
|
|
65
|
+
const dataPattern = /data\s+"([^"]+)"\s+"([^"]+)"/g;
|
|
66
|
+
while ((match = dataPattern.exec(cleanContent)) !== null) {
|
|
67
|
+
targets.push(`data.${match[1]}.${match[2]}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Pattern for module blocks: module "name"
|
|
71
|
+
const modulePattern = /module\s+"([^"]+)"/g;
|
|
72
|
+
while ((match = modulePattern.exec(cleanContent)) !== null) {
|
|
73
|
+
targets.push(`module.${match[1]}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return targets;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extracts targets from one or more terraform files
|
|
81
|
+
*/
|
|
82
|
+
const extractTargetsFromFiles = (files) => {
|
|
83
|
+
const allTargets = [];
|
|
84
|
+
const fileList = Array.isArray(files) ? files : [files];
|
|
85
|
+
|
|
86
|
+
fileList.forEach(filename => {
|
|
87
|
+
if (!fs.existsSync(filename)) {
|
|
88
|
+
console.log(`${P_WARN}Warning: File '${filename}' not found, skipping${P_END}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const content = fs.readFileSync(filename, 'utf8');
|
|
93
|
+
const targets = extractTargets(content);
|
|
94
|
+
|
|
95
|
+
if (targets.length > 0) {
|
|
96
|
+
console.log(`${P_INFO}Found ${targets.length} target(s) in '${filename}':${P_END}`);
|
|
97
|
+
targets.forEach(target => {
|
|
98
|
+
console.log(` - ${target}`);
|
|
99
|
+
if (!allTargets.includes(target)) {
|
|
100
|
+
allTargets.push(target);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
console.log(`${P_WARN}No targets found in '${filename}'${P_END}`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return allTargets;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Runs a terraform command (plan, apply, destroy) with optional file-based targets
|
|
113
|
+
* @param {string} command - The terraform command (plan, apply, destroy)
|
|
114
|
+
* @param {string|string[]} files - Optional file(s) to extract targets from
|
|
115
|
+
* @param {string[]} extraArgs - Additional arguments to pass to terraform
|
|
116
|
+
*/
|
|
117
|
+
exports.runTerraformCommand = async (command, files, extraArgs = []) => {
|
|
118
|
+
try {
|
|
119
|
+
const args = [command];
|
|
120
|
+
|
|
121
|
+
// Extract targets from files if provided
|
|
122
|
+
if (files && (Array.isArray(files) ? files.length > 0 : true)) {
|
|
123
|
+
console.log(`${P_OK}Extracting targets from file(s)...${P_END}\n`);
|
|
124
|
+
const targets = extractTargetsFromFiles(files);
|
|
125
|
+
|
|
126
|
+
if (targets.length > 0) {
|
|
127
|
+
console.log(`\n${P_OK}Total unique targets: ${targets.length}${P_END}\n`);
|
|
128
|
+
targets.forEach(target => {
|
|
129
|
+
args.push('-target', target);
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
console.log(`${P_WARN}No targets extracted from files. Running without file-based targets.${P_END}\n`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add any extra arguments passed through
|
|
137
|
+
if (extraArgs && extraArgs.length > 0) {
|
|
138
|
+
args.push(...extraArgs);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log(`${P_OK}Running: terraform ${args.join(' ')}${P_END}\n`);
|
|
142
|
+
|
|
143
|
+
// Execute terraform command
|
|
144
|
+
const tf = spawn('terraform', args, {
|
|
145
|
+
stdio: 'inherit',
|
|
146
|
+
cwd: process.cwd()
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
tf.on('error', (err) => {
|
|
150
|
+
console.log(`${P_ERROR}Error executing terraform: ${err.message}${P_END}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
tf.on('close', (code) => {
|
|
155
|
+
process.exit(code);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.log(`${P_ERROR}Error: ${err.message}${P_END}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
};
|