Skip to content
This repository was archived by the owner on Mar 19, 2024. It is now read-only.

Commit 2c10832

Browse files
ivan-kostkomr00wkanritholtz
authored
Custom destinations for modules (#13)
* Added destination option to configuration, so each module could be added to one or more specific destination folder --------- Co-authored-by: Rafal Przybyla <[email protected]> Co-authored-by: Nathaniel Ritholtz <[email protected]>
1 parent f625609 commit 2c10832

File tree

4 files changed

+242
-28
lines changed

4 files changed

+242
-28
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313

1414
# Terrafile output
1515
vendor/modules
16+
testdata
1617
terrafile
1718

1819
#IDEs
1920
.idea/
20-
dist/
21+
dist/
22+
.DS_Store

README.md

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ curl -L https://github.com/coretech/terrafile/releases/download/v{VERSION}/terra
2121
## How to use
2222
Terrafile expects a file named `Terrafile` which will contain your terraform module dependencies in a yaml like format.
2323

24-
An example Terrafile:
24+
There are two approaches that can be used for managing your modules depending on the structure of your terraform code:
25+
1. The default approach: `Terrafile` is located directly in the directory where terraform is run
26+
2. Centrally managed: `Terrafile` is located in "root" directory of your terraform code, managing modules in all subfolders / stacks
27+
28+
### Default Approach
29+
An example of default approach (#1) to `Terrafile`
2530
```
2631
tf-aws-vpc:
2732
source: "[email protected]:terraform-aws-modules/terraform-aws-vpc"
@@ -34,24 +39,75 @@ tf-aws-vpc-experimental:
3439
Terrafile config file in current directory and modules exported to ./vendor/modules
3540
```sh
3641
$ terrafile
37-
INFO[0000] [*] Checking out v1.46.0 of [email protected]:terraform-aws-modules/terraform-aws-vpc
38-
INFO[0000] [*] Checking out master of [email protected]:terraform-aws-modules/terraform-aws-vpc
42+
INFO[0000] [*] Checking out v1.46.0 of [email protected]:terraform-aws-modules/terraform-aws-vpc
43+
INFO[0000] [*] Checking out master of [email protected]:terraform-aws-modules/terraform-aws-vpc
3944
```
4045

4146
Terrafile config file in custom directory
4247
```sh
4348
$ terrafile -f config/Terrafile
44-
INFO[0000] [*] Checking out v1.46.0 of [email protected]:terraform-aws-modules/terraform-aws-vpc
45-
INFO[0000] [*] Checking out master of [email protected]:terraform-aws-modules/terraform-aws-vpc
49+
INFO[0000] [*] Checking out v1.46.0 of [email protected]:terraform-aws-modules/terraform-aws-vpc
50+
INFO[0000] [*] Checking out master of [email protected]:terraform-aws-modules/terraform-aws-vpc
4651
```
4752

4853
Terraform modules exported to custom directory
4954
```sh
5055
$ terrafile -p custom_directory
51-
INFO[0000] [*] Checking out master of [email protected]:terraform-aws-modules/terraform-aws-vpc
52-
INFO[0001] [*] Checking out v1.46.0 of [email protected]:terraform-aws-modules/terraform-aws-vpc
56+
INFO[0000] [*] Checking out master of [email protected]:terraform-aws-modules/terraform-aws-vpc
57+
INFO[0001] [*] Checking out v1.46.0 of [email protected]:terraform-aws-modules/terraform-aws-vpc
58+
```
59+
60+
### Centrally Managed Approach
61+
An example of using `Terrafile` in a root directory (#2):
62+
63+
Let's assume the following directory structure:
64+
65+
```
66+
.
67+
├── iam
68+
│   ├── main.tf
69+
│   └── .....tf
70+
├── networking
71+
│   ├── main.tf
72+
│   └── .....tf
73+
├── onboarding
74+
.
75+
.
76+
.
77+
├── some-other-stack
78+
└── Terrafile
79+
```
80+
81+
In the above scenario, Terrafile is not in every single folder but in the "root" of terraform code.
82+
83+
An example usage of centrally managed modules:
84+
85+
```
86+
tf-aws-vpc:
87+
source: "[email protected]:terraform-aws-modules/terraform-aws-vpc"
88+
version: "v1.46.0"
89+
destination:
90+
- networking
91+
tf-aws-iam:
92+
source: "[email protected]:terraform-aws-modules/terraform-aws-iam"
93+
version: "v5.11.1"
94+
destination:
95+
- iam
96+
tf-aws-s3-bucket:
97+
source: "[email protected]:terraform-aws-modules/terraform-aws-s3-bucket"
98+
version: "v3.6.1"
99+
destination:
100+
- networking
101+
- onboarding
102+
- some-other-stack
53103
```
54104

105+
The `destination` of module is an array of directories (stacks) where the module should be used.
106+
The module itself is fetched once and copied over to designated destinations.
107+
Final destination of the module is handled in a similar way as in first approach: `$destination/$module_path/$module_key`.
108+
109+
The output of the run is exactly the same in both options.
110+
55111
## TODO
56112
* Break out the main logic into seperate commands (e.g. version, help, run)
57113
* Update tests to include unit tests for broken out commands

main.go

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package main
1818

1919
import (
2020
"fmt"
21-
"io/ioutil"
2221
"os"
2322
"os/exec"
2423
"path/filepath"
@@ -31,12 +30,13 @@ import (
3130
)
3231

3332
type module struct {
34-
Source string `yaml:"source"`
35-
Version string `yaml:"version"`
33+
Source string `yaml:"source"`
34+
Version string `yaml:"version"`
35+
Destinations []string `yaml:"destinations"`
3636
}
3737

3838
var opts struct {
39-
ModulePath string `short:"p" long:"module_path" default:"./vendor/modules" description:"File path to install generated terraform modules"`
39+
ModulePath string `short:"p" long:"module_path" default:"./vendor/modules" description:"File path to install generated terraform modules, if not overridden by 'destinations:' field"`
4040

4141
TerrafilePath string `short:"f" long:"terrafile_file" default:"./Terrafile" description:"File path to the Terrafile file"`
4242
}
@@ -53,47 +53,105 @@ func init() {
5353
log.AddHook(stdemuxerhook.New(log.StandardLogger()))
5454
}
5555

56-
func gitClone(repository string, version string, moduleName string) {
56+
func gitClone(repository string, version string, moduleName string, destinationDir string) {
57+
cleanupPath := filepath.Join(destinationDir, moduleName)
58+
log.Printf("[*] Removing previously cloned artifacts at %s", cleanupPath)
59+
os.RemoveAll(cleanupPath)
5760
log.Printf("[*] Checking out %s of %s \n", version, repository)
5861
cmd := exec.Command("git", "clone", "--single-branch", "--depth=1", "-b", version, repository, moduleName)
59-
cmd.Dir = opts.ModulePath
60-
err := cmd.Run()
61-
if err != nil {
62-
log.Fatalln(err)
62+
cmd.Dir = destinationDir
63+
if err := cmd.Run(); err != nil {
64+
log.Fatalf("failed to clone repository %s due to error: %s", cmd.String(), err)
6365
}
6466
}
6567

6668
func main() {
69+
6770
fmt.Printf("Terrafile: version %v, commit %v, built at %v \n", version, commit, date)
6871
_, err := flags.Parse(&opts)
6972

7073
// Invalid choice
7174
if err != nil {
75+
log.Errorf("failed to parse flags due to: %s", err)
7276
os.Exit(1)
7377
}
7478

79+
workDirAbsolutePath, err := os.Getwd()
80+
if err != nil {
81+
log.Errorf("failed to get working directory absolute path due to: %s", err)
82+
}
83+
7584
// Read File
76-
yamlFile, err := ioutil.ReadFile(opts.TerrafilePath)
85+
yamlFile, err := os.ReadFile(opts.TerrafilePath)
7786
if err != nil {
78-
log.Fatalln(err)
87+
log.Fatalf("failed to read configuration in file %s due to error: %s", opts.TerrafilePath, err)
7988
}
8089

8190
// Parse File
8291
var config map[string]module
8392
if err := yaml.Unmarshal(yamlFile, &config); err != nil {
84-
log.Fatalln(err)
93+
log.Fatalf("failed to parse yaml file due to error: %s", err)
8594
}
8695

8796
// Clone modules
8897
var wg sync.WaitGroup
8998
_ = os.RemoveAll(opts.ModulePath)
9099
_ = os.MkdirAll(opts.ModulePath, os.ModePerm)
100+
91101
for key, mod := range config {
92102
wg.Add(1)
93103
go func(m module, key string) {
94104
defer wg.Done()
95-
gitClone(m.Source, m.Version, key)
96-
_ = os.RemoveAll(filepath.Join(opts.ModulePath, key, ".git"))
105+
106+
// path to clone module
107+
cloneDestination := opts.ModulePath
108+
// list of paths to link module to. empty, unless Destinations are more than 1 location
109+
var linkDestinations []string
110+
111+
if m.Destinations != nil && len(m.Destinations) > 0 {
112+
// set first in Destinations as location to clone to
113+
cloneDestination = filepath.Join(m.Destinations[0], opts.ModulePath)
114+
// the rest of Destinations are locations to link module to
115+
linkDestinations = m.Destinations[1:]
116+
117+
}
118+
119+
// create folder to clone into
120+
if err := os.MkdirAll(cloneDestination, os.ModePerm); err != nil {
121+
log.Errorf("failed to create folder %s due to error: %s", cloneDestination, err)
122+
123+
// no reason to continue as failed to create folder
124+
return
125+
}
126+
127+
// clone repository
128+
gitClone(m.Source, m.Version, key, cloneDestination)
129+
130+
for _, d := range linkDestinations {
131+
// the source location as folder where module was cloned and module folder name
132+
moduleSrc := filepath.Join(workDirAbsolutePath, cloneDestination, key)
133+
// append destination path with module path
134+
dst := filepath.Join(d, opts.ModulePath)
135+
136+
log.Infof("[*] Creating folder %s", dst)
137+
if err := os.MkdirAll(dst, os.ModePerm); err != nil {
138+
log.Errorf("failed to create folder %s due to error: %s", dst, err)
139+
return
140+
}
141+
142+
dst = filepath.Join(dst, key)
143+
144+
log.Infof("[*] Remove existing artifacts at %s", dst)
145+
if err := os.RemoveAll(dst); err != nil {
146+
log.Errorf("failed to remove location %s due to error: %s", dst, err)
147+
return
148+
}
149+
150+
log.Infof("[*] Link %s to %s", moduleSrc, dst)
151+
if err := os.Symlink(moduleSrc, dst); err != nil {
152+
log.Errorf("failed to link module from %s to %s due to error: %s", moduleSrc, dst, err)
153+
}
154+
}
97155
}(mod, key)
98156
}
99157

0 commit comments

Comments
 (0)