terraspace-bundler 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +9 -89
  4. data/lib/terraspace_bundler/cli/bundle.rb +2 -2
  5. data/lib/terraspace_bundler/cli/help/bundle/info.md +1 -1
  6. data/lib/terraspace_bundler/config.rb +3 -2
  7. data/lib/terraspace_bundler/dsl/syntax.rb +13 -9
  8. data/lib/terraspace_bundler/exporter.rb +7 -17
  9. data/lib/terraspace_bundler/exporter/base.rb +6 -0
  10. data/lib/terraspace_bundler/exporter/copy.rb +25 -0
  11. data/lib/terraspace_bundler/exporter/stacks.rb +14 -0
  12. data/lib/terraspace_bundler/exporter/stacks/base.rb +5 -0
  13. data/lib/terraspace_bundler/exporter/stacks/rewrite.rb +57 -0
  14. data/lib/terraspace_bundler/exporter/stacks/stack.rb +77 -0
  15. data/lib/terraspace_bundler/lockfile.rb +4 -6
  16. data/lib/terraspace_bundler/lockfile/version_comparer.rb +31 -12
  17. data/lib/terraspace_bundler/lockfile/yamler.rb +1 -1
  18. data/lib/terraspace_bundler/mod.rb +6 -9
  19. data/lib/terraspace_bundler/mod/downloader.rb +24 -5
  20. data/lib/terraspace_bundler/mod/path_concern.rb +5 -0
  21. data/lib/terraspace_bundler/mod/props_builder.rb +1 -2
  22. data/lib/terraspace_bundler/mod/registry.rb +4 -1
  23. data/lib/terraspace_bundler/mod/stack_concern.rb +44 -0
  24. data/lib/terraspace_bundler/{installer.rb → runner.rb} +5 -5
  25. data/lib/terraspace_bundler/syncer.rb +6 -6
  26. data/lib/terraspace_bundler/terrafile.rb +1 -2
  27. data/lib/terraspace_bundler/version.rb +1 -1
  28. data/spec/fixtures/rewrite/example-subfolder.tf +3 -0
  29. data/spec/fixtures/rewrite/module-subfolder.tf +3 -0
  30. data/spec/fixtures/rewrite/no-leading.tf +3 -0
  31. data/spec/fixtures/rewrite/no-path.tf +8 -0
  32. data/spec/fixtures/rewrite/no-spaces.tf +3 -0
  33. data/spec/fixtures/rewrite/normal.tf +3 -0
  34. data/spec/fixtures/rewrite/spaces.tf +3 -0
  35. data/spec/fixtures/rewrite/trailing-slash.tf +3 -0
  36. data/spec/spec_helper.rb +4 -0
  37. data/spec/terraform_bundler/exporter/stacks/rewrite_spec.rb +86 -0
  38. data/spec/terraform_bundler/mod_spec.rb +75 -0
  39. data/spec/terraform_bundler/runner_spec.rb +1 -1
  40. metadata +30 -5
  41. data/lib/terraspace_bundler/cli/runner.rb +0 -7
  42. data/lib/terraspace_bundler/dsl/concern.rb +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 500271ff2f8e5799a55febd3ec25261bcf4126bff456b83360ae0e2a96bb6357
4
- data.tar.gz: ee45f81805b893d5287fdea6777c9b28ac39936273806c3d97275f6c314a8cd1
3
+ metadata.gz: b27c1a0f9a31f2e9d7e32c8c098c7be4b677f5109a0283063928a724c42cd54c
4
+ data.tar.gz: cab661fe23294051ae93b5686bf1a7ee36b453795a422a3c47a9c4154af732eb
5
5
  SHA512:
6
- metadata.gz: 94af4d341ed3e43445bf2eb0eae1e3383fe064a6df708c449658b8eb5a958163a225be2ddae121356573ae7e099e87669c5cb9c54808423b9db685faf27d7837
7
- data.tar.gz: d68d1825f89b91ac4d9ad7525a4b4b95cc709ce1e2f3c10d83aa6ad5c6b27a89b3c6ae1ad9bc1ee3179b08345ac73fc551d1467769f702cc43d69b6f3b38b577
6
+ metadata.gz: 52f69be90c325355378fb47ce8f75873a13ade4b228494177bdc770d996b4b86f35b156c96a77fa2195c7f1f5d0432f38bde1b9330398ce611d0c471b2646610
7
+ data.tar.gz: eaad3baf724bf7a0da132257e3e3d59dd9b6305b194c61bdbde0a9d90cf741b47cf163b1524ec05ee83e1f8dad81cd5ca9356b78635ba096eaec1ac5570737f5
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  This project *loosely tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
5
 
6
+ ## [0.3.0]
7
+ - change `base_clone_url` default from `git@github.com:` to `https://github.com/`
8
+ - mod-level stack option
9
+ - terrafile-level stack_options
10
+ - rename terrafile-level option to export_to
11
+
6
12
  ## [0.2.0]
7
13
  - #1 Rework implementation. Better Terrafile syncing
8
14
  - clearer implementation: Terrafile and Lockfile classes build standard mods object.
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # terraspace-bundler
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/terraspace-bundler.png)](http://badge.fury.io/rb/terraspace-bundler)
4
+
5
+ [![BoltOps Badge](https://img.boltops.com/boltops/badges/boltops-badge.png)](https://www.boltops.com)
6
+
3
7
  Bundles terraform modules based on a `Terrafile` to the `vendor/modules` folder. Used by the [Terraspace Terraform Framework](https://terraspace.cloud/).
4
8
 
5
9
  ## Usage
@@ -7,14 +11,13 @@ Bundles terraform modules based on a `Terrafile` to the `vendor/modules` folder.
7
11
  Create a `Terrafile` file:
8
12
 
9
13
  ```ruby
10
- org "boltopspro" # set default org
14
+ org "boltops-tools" # set default org
11
15
 
12
16
  # GitHub repo with default org
13
- mod "elb", source: "terraform-aws-elb", version: "master"
14
- mod "ec2", source: "terraform-aws-ec2", version: "branch-name"
17
+ mod "s3", source: "terraform-aws-s3", version: "master"
15
18
 
16
19
  # GitHub repo with explicit org
17
- mod "rds", source: "boltopspro/terraform-aws-rds", version: "v0.1.0"
20
+ # mod "elasticache", source: "boltopspro/terraform-aws-elasticache"
18
21
 
19
22
  # Terraform registry
20
23
  mod "sg", source: "terraform-aws-modules/security-group/aws", version: "3.10.0"
@@ -26,90 +29,7 @@ Running `terraspace bundle` creates the `Terrafile.lock` file, which locks the v
26
29
 
27
30
  terraspace bundle
28
31
 
29
- ## Updating
30
-
31
- To update all the locked versions in `Terrafile.lock` run `terraspace bundle update`. You can also delete the `Terrafile.lock` file first.
32
-
33
- terraspace bundle update
34
-
35
- You can selectively update multiple modules:
36
-
37
- terraspace bundle update MODS
38
- terraspace bundle update mod1 mod2
39
-
40
- ## List & Info
41
-
42
- terraspace bundle list
43
- terraspace bundle info mod1
44
-
45
- ## DSL Methods
46
-
47
- Some of these are methods that apply globally at the Terrafile level. Some of these are options that apply the at the mod method level.
48
-
49
- ### Terrafile level
50
-
51
- Name | Description | Default
52
- --- | --- | ---
53
- base_clone_url | Base clone url to use | git@gihtub.com:
54
- export_path | Where the modules get exported to saved to. | vendor/modules
55
- export_purge | Where or not to clean up all existing exported modules folder first. | true
56
-
57
- ### Mod Method level
58
-
59
- Name | Description | Default
60
- --- | --- | ---
61
- subfolder | The subfolder where the module lives within the repo. | nil
62
- export_to | Overrides the export_path Terrafile level option. With this one-off option, other modules in this folder will not be purged. | nil
63
-
64
- Examples of some of the options are below:
65
-
66
- ### base url
67
-
68
- The base url used for clone is `git@github.com:`. You can change it with `base_url`, example:
69
-
70
- ```ruby
71
- base_url "https://github.com/" # default base url for git clone
72
- # ...
73
- ```
74
-
75
- ### export_path
76
-
77
- The default export path is `vendor/modules`, you can change it:
78
-
79
- ```ruby
80
- export_path "app/modules"
81
- # ...
82
- ```
83
-
84
- ## prune
85
-
86
- The `terraspace bundle` commands removes all files in the export_path, `vendor/modules`, by default. You can disable this behavior with:
87
-
88
- ```ruby
89
- export_purge false
90
- # ...
91
- ```
92
-
93
- ### subfolder
94
-
95
- The default export path is `vendor/modules`, you can change it:
96
-
97
- ```ruby
98
- # ...
99
- mod "s3", source: "terraform-aws-s3", subfolder: "path/to/module/s3"
100
- ```
101
-
102
- ## Config Options
103
-
104
- You can also configure some behavior with `TB.config`. IE: `TB.config.terrafile = "/path/to/Terrafile"`
105
-
106
- Name | Description | Default
107
- --- | --- | ---
108
- base_clone_url | Base git url to use for cloning | git@github.com:
109
- export_path | Where to export the modules to. Can also be set with the env var TB_EXPORT_PATH | vendor/modules
110
- export_purge | Whether or not to prune all the modules | true
111
- logger | Logger instance
112
- terrafile | The Terrafile path. Can also be set with the env TB_TERRAFILE || "Terrafile"
32
+ For more detailed usage instructions refer to the [Terraspace Terrafile docs](https://terraspace.cloud/docs/terrafile/options/)
113
33
 
114
34
  ## Installation
115
35
 
@@ -123,7 +43,7 @@ To install:
123
43
  * Handles updating the `Terrafile.lock` based on the `Terrafile`
124
44
  * Others running the `terraspace bundle install` will install the exact same module versions based the `Terrafile.lock`.
125
45
  * To update `Terraform.lock` run `terraspace bundle update`.
126
- * The repos are downloaded to `/tmp/terraspace-bundler` area as a cache. Delete the cache by running `terraspace bundle purge_cache`.
46
+ * The repos are downloaded to the `/tmp/terraspace-bundler` area as a cache. Delete the cache by running `terraspace bundle purge_cache`.
127
47
 
128
48
  ## Contributing
129
49
 
@@ -22,7 +22,7 @@ class TerraspaceBundler::CLI
22
22
  long_desc Help.text("bundle/install")
23
23
  terrafile_option.call
24
24
  def install
25
- Runner.new(options).run
25
+ TB::Runner.new(options).run
26
26
  end
27
27
 
28
28
  desc "purge_cache", "Purge cache."
@@ -35,7 +35,7 @@ class TerraspaceBundler::CLI
35
35
  long_desc Help.text("bundle/update")
36
36
  terrafile_option.call
37
37
  def update(*mods)
38
- Runner.new(options.merge(mods: mods, mode: "update")).run
38
+ TB::Runner.new(options.merge(mods: mods, mode: "update")).run
39
39
  end
40
40
  end
41
41
  end
@@ -5,4 +5,4 @@
5
5
  sha: bad676a426f5d3ddb48e674bebd6fd52f063a8b4
6
6
  source: org/terraform-random-pet
7
7
  type: git
8
- url: git@github.com:org/terraform-random-pet
8
+ url: https://github.com/org/terraform-random-pet
@@ -5,11 +5,12 @@ module TerraspaceBundler
5
5
 
6
6
  def config
7
7
  config = ActiveSupport::OrderedOptions.new
8
- config.base_clone_url = "git@github.com:"
9
- config.export_path = ENV['TB_EXPORT_PATH'] || "vendor/modules"
8
+ config.base_clone_url = "https://github.com/"
9
+ config.export_to = ENV['TB_EXPORT_TO'] || "vendor/modules"
10
10
  config.export_purge = ENV['TB_EXPORT_PRUNE'] == '0' ? false : true
11
11
  config.lockfile = "#{config.terrafile}.lock"
12
12
  config.logger = new_logger
13
+ config.stack_options = {dest: "app/stacks", purge: nil, examples: "examples"} # Note: Important purge is nil not false so can fallback to Terrafile-level stack_options
13
14
  config.terrafile = ENV['TB_TERRAFILE'] || "Terrafile"
14
15
  config
15
16
  end
@@ -1,28 +1,32 @@
1
1
  class TerraspaceBundler::Dsl
2
2
  module Syntax
3
3
  def org(url)
4
- global[:org] = url
4
+ config.org = url
5
5
  end
6
6
  alias_method :user, :org
7
7
 
8
8
  def base_clone_url(value)
9
- TB.config.base_clone_url = value
9
+ config.base_clone_url = value
10
10
  end
11
11
 
12
- def export_path(path)
13
- global[:export_path] = path
12
+ def export_to(path)
13
+ config.export_to = path
14
14
  end
15
15
 
16
16
  def export_purge(value)
17
- TB.config.export_purge = value
17
+ config.export_purge = value
18
18
  end
19
19
 
20
- def mod(*args, **options)
21
- meta[:mods] << {args: args, options: options}
20
+ def stack_options(value={})
21
+ config.stack_options.merge!(value)
22
+ end
23
+
24
+ def config
25
+ TB.config
22
26
  end
23
27
 
24
- def global
25
- meta[:global]
28
+ def mod(*args, **options)
29
+ meta[:mods] << {args: args, options: options}
26
30
  end
27
31
  end
28
32
  end
@@ -1,6 +1,5 @@
1
1
  module TerraspaceBundler
2
2
  class Exporter
3
- include TB::Mod::PathConcern
4
3
  include TB::Util::Logging
5
4
 
6
5
  def initialize(options={})
@@ -10,34 +9,25 @@ module TerraspaceBundler
10
9
  def run
11
10
  purge
12
11
  lockfile.mods.each do |mod|
12
+ logger.info "Exporting #{mod.name}"
13
13
  export(mod)
14
14
  end
15
15
  end
16
16
 
17
17
  def export(mod)
18
18
  downloader = Mod::Downloader.new(mod)
19
+ downloader.run
19
20
  downloader.switch_version(mod.sha)
20
-
21
- stage_path = stage_path(mod.full_repo)
22
- stage_path = "#{stage_path}/#{mod.subfolder}" if mod.subfolder
23
- mod_path = mod_path(mod)
24
- FileUtils.rm_rf(mod_path)
25
- FileUtils.mkdir_p(File.dirname(mod_path))
26
- FileUtils.cp_r(stage_path, mod_path)
27
- FileUtils.rm_rf("#{mod_path}/.git")
28
- logger.debug "Exported: #{mod_path}"
29
- end
30
-
31
- def mod_path(mod)
32
- name = mod.name
33
- export_to = mod.export_to || TB.config.export_path
34
- "#{export_to}/#{name}"
21
+ copy = Copy.new(mod)
22
+ copy.mod
23
+ copy.stacks
24
+ logger.debug "Exported: #{copy.mod_path}"
35
25
  end
36
26
 
37
27
  private
38
28
  def purge
39
29
  return unless TB.config.export_purge
40
- FileUtils.rm_rf(TB.config.export_path)
30
+ FileUtils.rm_rf(TB.config.export_to)
41
31
  end
42
32
 
43
33
  def lockfile
@@ -0,0 +1,6 @@
1
+ class TerraspaceBundler::Exporter
2
+ class Base
3
+ include TB::Mod::PathConcern
4
+ include TB::Util::Logging
5
+ end
6
+ end
@@ -0,0 +1,25 @@
1
+ class TerraspaceBundler::Exporter
2
+ class Copy < Base
3
+ def initialize(mod)
4
+ @mod = mod
5
+ end
6
+
7
+ def mod
8
+ FileUtils.rm_rf(mod_path)
9
+ FileUtils.mkdir_p(File.dirname(mod_path))
10
+ FileUtils.cp_r(src_path, mod_path)
11
+ FileUtils.rm_rf("#{mod_path}/.git")
12
+ end
13
+
14
+ def stacks
15
+ Stacks.new(@mod).export
16
+ end
17
+
18
+ # src path is from the stage area
19
+ def src_path
20
+ path = stage_path(@mod.full_repo) # from PathConcern
21
+ path = "#{path}/#{@mod.subfolder}" if @mod.subfolder
22
+ path
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ class TerraspaceBundler::Exporter
2
+ class Stacks < Base
3
+ def initialize(mod)
4
+ @mod = mod
5
+ @stacks = mod.stacks || []
6
+ end
7
+
8
+ def export
9
+ @stacks.each do |options|
10
+ Stack.new(@mod, options).export
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ class TerraspaceBundler::Exporter::Stacks
2
+ class Base < TerraspaceBundler::Exporter::Base
3
+ include TB::Util::Logging
4
+ end
5
+ end
@@ -0,0 +1,57 @@
1
+ class TerraspaceBundler::Exporter::Stacks
2
+ class Rewrite < Base
3
+ def initialize(stack)
4
+ @stack = stack
5
+ end
6
+
7
+ def run
8
+ expr = "#{@stack.dest}/*.tf"
9
+ Dir.glob(expr).each do |path|
10
+ next unless File.file?(path) # skip symlinks and dirs
11
+ replace(path)
12
+ end
13
+ end
14
+
15
+ def replace(path)
16
+ lines = IO.readlines(path)
17
+ new_lines = new_lines(lines)
18
+ text = new_lines.join('')
19
+ IO.write(path, text)
20
+ end
21
+
22
+ def new_lines(lines)
23
+ lines.map do |line|
24
+ # leading spaces or no space. but cannot have any other chars
25
+ # can have space betweens source and =
26
+ line =~ /^\s*source\s*=/ ? new_line(line) : line
27
+ end
28
+ end
29
+
30
+ def new_line(line)
31
+ # marker comment so files dont get updated twice when purge is false
32
+ terraspace_comment = "# updated by terraspace"
33
+ return line unless line.include?("../")
34
+ return line if line.include?(terraspace_comment)
35
+
36
+ md = line.match(/^\s*source\s*=\s*"(.*)?"/)
37
+ unless md
38
+ logger.error "ERROR: unable to find the module source".color(:red)
39
+ logger.error line
40
+ exit 1
41
+ end
42
+ # Lots of cases covered by specs
43
+ #
44
+ # ../..
45
+ # ../../
46
+ # ../../modules/iam-user
47
+ # ../../../modules/compute_instance
48
+ #
49
+ source_value = md[1] # IE: ""../../../modules/compute_instance"
50
+ dirs = source_value.split('/')
51
+ count = dirs.count('..')
52
+ mod_dir = dirs[count..-1].join('/')
53
+ full_dir = [@stack.mod.name, mod_dir].reject(&:empty?).join('/')
54
+ %Q| source = "../../modules/#{full_dir}" #{terraspace_comment}\n|
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,77 @@
1
+ class TerraspaceBundler::Exporter::Stacks
2
+ class Stack < Base
3
+ attr_reader :mod
4
+ def initialize(mod, options={})
5
+ @mod, @options = mod, options
6
+ end
7
+
8
+ def export
9
+ copy
10
+ rewrite
11
+ end
12
+
13
+ def copy
14
+ return unless @options
15
+
16
+ FileUtils.rm_rf(dest) if purge?
17
+ return if File.exist?(dest)
18
+
19
+ FileUtils.mkdir_p(File.dirname(dest))
20
+ FileUtils.cp_r(src, dest)
21
+ end
22
+
23
+ def rewrite
24
+ Rewrite.new(self).run
25
+ end
26
+
27
+ def src
28
+ src = @options[:src]
29
+ without_examples = [mod_path, src].compact.join('/')
30
+ with_examples = [examples_folder, src].compact.join('/')
31
+ paths = [with_examples, without_examples]
32
+ found = paths.find do |path|
33
+ File.exist?(path)
34
+ end
35
+
36
+ unless found
37
+ searched = paths.map { |p| pretty_path(p) }.map { |p| " #{p}" }.join("\n")
38
+ logger.error "ERROR: Example not found. stack src: #{src}. Searched:".color(:red)
39
+ logger.error searched
40
+ exit 1
41
+ end
42
+ found
43
+ end
44
+
45
+ def pretty_path(path)
46
+ path.sub("#{Dir.pwd}/",'')
47
+ end
48
+
49
+ # public method used by StackConcern#all_stacks
50
+ def examples_folder
51
+ [mod_path, examples].join('/')
52
+ end
53
+
54
+ def examples
55
+ @options[:examples] || TB.config.stack_options[:examples]
56
+ end
57
+
58
+ def dest
59
+ dest = @options[:dest] || TB.config.stack_options[:dest]
60
+ name = @options[:name] || @mod.name # falls back to mod name by default
61
+ "#{dest}/#{name}"
62
+ end
63
+
64
+ # purge precedence:
65
+ #
66
+ # 1. Terrafile mod level stack option
67
+ # 2. Terrafile-level stack_options
68
+ #
69
+ def purge?
70
+ # config.stack_options is set from Terrafile-level stack_options to TB.config.stack
71
+ # relevant source code: dsl/syntax.rb: def stack_options
72
+ config = TB.config.stack_options[:purge]
73
+ config = config.nil? ? false : config
74
+ @options[:purge].nil? ? config : @options[:purge]
75
+ end
76
+ end
77
+ end
@@ -8,11 +8,11 @@ module TerraspaceBundler
8
8
  # {"vpc"=>
9
9
  # {"sha"=>"52328b2b5197f9b95e3005cfcfb99595040ee45b",
10
10
  # "source"=>"org/terraform-aws-vpc",
11
- # "url"=>"git@github.com:org/terraform-aws-vpc"},
11
+ # "url"=>"https://github.com/org/terraform-aws-vpc"},
12
12
  # "instance"=>
13
13
  # {"sha"=>"570cca3ea7b25e3af1961dc57b27ca2c129b934a",
14
14
  # "source"=>"org/terraform-aws-instance",
15
- # "url"=>"git@github.com:org/terraform-aws-instance"}}
15
+ # "url"=>"https://github.com/org/terraform-aws-instance"}}
16
16
  @@mods = nil
17
17
  def mods
18
18
  return @@mods if @@mods
@@ -29,9 +29,7 @@ module TerraspaceBundler
29
29
 
30
30
  # update (if version mismatch) or create (if missing)
31
31
  def sync(mod)
32
- changed = changed?(mod)
33
- logger.debug "Detecting change for mod #{mod.name} changed #{changed.inspect}"
34
- replace!(mod) if changed
32
+ replace!(mod) if changed?(mod)
35
33
  end
36
34
 
37
35
  # mod built from Terrafile
@@ -45,7 +43,7 @@ module TerraspaceBundler
45
43
 
46
44
  comparer = VersionComparer.new(found, mod)
47
45
  comparer.run
48
- logger.debug(comparer.reason) if comparer.reason
46
+ logger.debug("REASON: #{comparer.reason}") if comparer.reason
49
47
  comparer.changed?
50
48
  end
51
49
 
@@ -10,9 +10,24 @@ class TerraspaceBundler::Lockfile
10
10
  @changed
11
11
  end
12
12
 
13
+ # Tricky logic, maybe spec this.
14
+ #
15
+ # no mods specified:
16
+ # terraspace bundle update # no mods specified => update all
17
+ # terraspace bundle install # no Terrafile.lock => update all
18
+ # mods specified:
19
+ # terraspace bundle update s3 # explicit mod => update s3
20
+ # terraspace bundle install s3 # errors: not possible to specify module for install command
21
+ #
22
+ # Note: Install with specific mods wipes existing mods. Not worth it to support.
23
+ #
13
24
  def run
14
25
  @changed = false
15
- strict_versions = %w[subfolder ref tag export_to]
26
+
27
+ # Most props are "strict" version checks. So if user changes options generally in the mod line
28
+ # the Terrafile.lock will get updated, which is expected behavior.
29
+ props = @locked.props.keys + @current.props.keys
30
+ strict_versions = props.uniq.sort - [:sha]
16
31
  strict_versions.each do |version|
17
32
  @changed = @locked.send(version) != @current.send(version)
18
33
  if @changed
@@ -21,23 +36,27 @@ class TerraspaceBundler::Lockfile
21
36
  end
22
37
  end
23
38
 
24
- # Loose version checks work a little different. If not set it explicitly, they will not be checked.
25
- # Will use locked version in Terrafile.lock in this case.
26
- # Note: Also, check the sha last since it triggers a git fetch.
27
- loose_versions = %w[branch sha]
28
- loose_versions.each do |version|
29
- @changed = @current.send(version) && @current.send(version) != @locked.send(version)
30
- if @changed
31
- @reason = reason_message(version)
32
- return @changed
33
- end
39
+ # Lots of nuance with the sha check that works differently
40
+ # Only check when set.
41
+ # Also in update mode then always check it.
42
+ @changed = @current.sha && !@locked.sha.include?(@current.sha) ||
43
+ update_mode? && !@current.latest_sha.include?(@locked.sha)
44
+ if @changed
45
+ @reason = reason_message("sha")
46
+ return @changed
34
47
  end
35
48
 
36
49
  @changed
37
50
  end
38
51
 
39
52
  def reason_message(version)
40
- "Replacing mod: #{@current.name}. #{version} is different in Terrafile and Terrafile.lock"
53
+ "Replacing mod #{@current.name} because #{version} is different in Terrafile and Terrafile.lock"
54
+ end
55
+
56
+ def update_mode?
57
+ self.class.update_mode
41
58
  end
59
+
60
+ class_attribute :update_mode
42
61
  end
43
62
  end
@@ -19,7 +19,7 @@ class TerraspaceBundler::Lockfile
19
19
  def item(mod)
20
20
  props = mod.props.dup # passthrough: name, url, version, ref, tag, branch etc
21
21
  props.delete(:name) # different structure in Terrafile.lock YAML
22
- props[:sha] = mod.sha
22
+ props[:sha] ||= mod.latest_sha
23
23
  props.delete_if { |k,v| v.nil? }
24
24
  { mod.name => props }
25
25
  end
@@ -1,7 +1,9 @@
1
1
  module TerraspaceBundler
2
2
  class Mod
3
3
  extend PropsExtension
4
- props :name, :source, :url, :subfolder, :type, :export_to
4
+ props :export_to, :name, :sha, :source, :subfolder, :type, :url
5
+
6
+ include StackConcern
5
7
 
6
8
  attr_reader :props, :version, :ref, :tag, :branch
7
9
  def initialize(props={})
@@ -10,12 +12,6 @@ module TerraspaceBundler
10
12
  @version, @ref, @tag, @branch = @props[:version], @props[:ref], @props[:tag], @props[:branch]
11
13
  end
12
14
 
13
- def sha
14
- # sha will already be set if coming from Terrafile.lock
15
- # sha will be lazily fetch set if coming from Terrafile
16
- @props[:sha] ||= fetch_sha
17
- end
18
-
19
15
  def checkout_version
20
16
  v = detected_version
21
17
  v = "v#{v}" if type == "registry" && @version && !v.starts_with?("v")
@@ -35,12 +31,13 @@ module TerraspaceBundler
35
31
  "#{org}/#{repo}"
36
32
  end
37
33
 
38
- private
39
- def fetch_sha
34
+ def latest_sha
40
35
  downloader = Downloader.new(self)
41
36
  downloader.run
42
37
  downloader.sha
43
38
  end
39
+
40
+ private
44
41
  # support variety of options, prefer version
45
42
  def detected_version
46
43
  @version || @ref || @tag || @branch
@@ -16,9 +16,7 @@ class TerraspaceBundler::Mod
16
16
  FileUtils.mkdir_p(org_path)
17
17
 
18
18
  Dir.chdir(org_path) do
19
- unless File.exist?(@mod.repo)
20
- sh "git clone #{@mod.url}"
21
- end
19
+ clone unless File.exist?(@mod.repo)
22
20
 
23
21
  Dir.chdir(@mod.repo) do
24
22
  git "pull"
@@ -27,14 +25,35 @@ class TerraspaceBundler::Mod
27
25
  end
28
26
  end
29
27
  end
28
+ memoize :run
29
+
30
+ def clone
31
+ sh "git clone #{@mod.url}"
32
+ rescue TB::GitError => e
33
+ logger.error "ERROR: #{e.message}".color(:red)
34
+ exit 1
35
+ end
30
36
 
31
37
  def stage
32
38
  copy_to_stage
33
- # TODO: if there's no master, need to checkout if its the default branch
34
- checkout = @mod.checkout_version || "master"
39
+ checkout = @mod.checkout_version || default_branch
35
40
  switch_version(checkout)
36
41
  end
37
42
 
43
+ # Note: if not in a git repo, will get this error message in stderr
44
+ #
45
+ # fatal: Not a git repository (or any of the parent directories): .git
46
+ #
47
+ def default_branch
48
+ origin = `git remote show origin`.strip
49
+ found = origin.split("\n").find { |l| l.include?("HEAD") }
50
+ if found
51
+ found.split(':').last.strip
52
+ else
53
+ 'master'
54
+ end
55
+ end
56
+
38
57
  def switch_version(version)
39
58
  stage_path = stage_path("#{@mod.org}/#{@mod.repo}")
40
59
  logger.debug "Within: #{stage_path}"
@@ -24,5 +24,10 @@ class TerraspaceBundler::Mod
24
24
  def stage_path(name)
25
25
  "#{stage_root}/#{name}"
26
26
  end
27
+
28
+ def mod_path
29
+ export_to = @mod.export_to || TB.config.export_to
30
+ "#{export_to}/#{@mod.name}"
31
+ end
27
32
  end
28
33
  end
@@ -3,7 +3,6 @@
3
3
  class TerraspaceBundler::Mod
4
4
  class PropsBuilder
5
5
  extend Memoist
6
- include TerraspaceBundler::Dsl::Concern
7
6
 
8
7
  def initialize(params={})
9
8
  @params = params
@@ -28,7 +27,7 @@ class TerraspaceBundler::Mod
28
27
  if registry?
29
28
  @source
30
29
  else
31
- @source.include?('/') ? @source : "#{dsl.global[:org]}/#{@source}"
30
+ @source.include?('/') ? @source : "#{TB.config.org}/#{@source}"
32
31
  end
33
32
  end
34
33
 
@@ -3,6 +3,8 @@ require 'open-uri'
3
3
 
4
4
  class TerraspaceBundler::Mod
5
5
  class Registry
6
+ include TB::Util::Logging
7
+
6
8
  def initialize(source, version)
7
9
  @source, @version = source, version
8
10
  end
@@ -41,7 +43,8 @@ class TerraspaceBundler::Mod
41
43
  resp = http_request(next_url)
42
44
  download_url = resp.header["x-terraform-get"]
43
45
  else
44
- raise "Unable to lookup up module in Terraform Registry: #{resp}"
46
+ logger.error "ERROR: Unable to lookup up module in Terraform Registry: #{@source}".color(:red)
47
+ exit 1
45
48
  end
46
49
 
47
50
  url = download_url.sub(%r{/archive/.*},'')
@@ -0,0 +1,44 @@
1
+ class TerraspaceBundler::Mod
2
+ module StackConcern
3
+ def stacks
4
+ stacks = @props[:stacks] || @props[:stack]
5
+ return unless stacks
6
+ if all_stacks?(stacks)
7
+ stacks = all_stacks
8
+ end
9
+ normalize_stacks(stacks)
10
+ end
11
+ alias_method :stack, :stacks # important to have alias for VersionCheck
12
+
13
+ def all_stacks?(*stacks)
14
+ stacks.flatten == [:all]
15
+ end
16
+
17
+ def all_stacks
18
+ stack = TerraspaceBundler::Exporter::Stacks::Stack.new(self) # to get the mod src path
19
+ expr = "#{stack.examples_folder}/*"
20
+ Dir.glob(expr).map do |path|
21
+ example = File.basename(path)
22
+ # Set name so multiple app/stacks are created instead of just one app/stack/MOD
23
+ {
24
+ name: example,
25
+ src: example,
26
+ }
27
+ end
28
+ end
29
+
30
+ # Normalizes stack options to an Array of Hashes or just a single Hash
31
+ def normalize_stacks(option)
32
+ defaults = TB.config.stack_options.dup
33
+ result = case option
34
+ when String
35
+ [defaults.merge(src: option)]
36
+ when Array
37
+ option.map! {|s| normalize_stacks(s) }
38
+ else # Hash
39
+ [defaults.merge!(option)]
40
+ end
41
+ result.flatten
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,5 @@
1
1
  module TerraspaceBundler
2
- class Installer < CLI::Base
2
+ class Runner < CLI::Base
3
3
  def run
4
4
  Syncer.new(@options).run
5
5
  Exporter.new(@options).run
@@ -8,10 +8,10 @@ module TerraspaceBundler
8
8
 
9
9
  def finish_message
10
10
  no_modules_found = true
11
- export_paths.each do |export_path|
12
- found = Dir.exist?(export_path) && !Dir.empty?(export_path)
11
+ export_paths.each do |path|
12
+ found = Dir.exist?(path) && !Dir.empty?(path)
13
13
  next unless found
14
- logger.info "Modules saved to #{export_path}"
14
+ logger.info "Modules saved to #{path}"
15
15
  no_modules_found = false
16
16
  end
17
17
 
@@ -20,7 +20,7 @@ module TerraspaceBundler
20
20
 
21
21
  def export_paths
22
22
  export_paths = Terrafile.instance.mods.map(&:export_to).compact.uniq
23
- export_paths << TB.config.export_path
23
+ export_paths << TB.config.export_to
24
24
  export_paths
25
25
  end
26
26
  end
@@ -1,7 +1,6 @@
1
1
  module TerraspaceBundler
2
2
  class Syncer
3
3
  include TB::Util::Logging
4
- include Dsl::Concern
5
4
 
6
5
  def initialize(options={})
7
6
  @options = options
@@ -31,17 +30,18 @@ module TerraspaceBundler
31
30
  end
32
31
 
33
32
  def sync_mods
33
+ # VersionComparer is used in lockfile.sync and does heavy lifting to check if mod should be updated and replaced
34
+ TB::Lockfile::VersionComparer.update_mode = @options[:mode] == "update"
34
35
  terrafile.mods.each do |mod|
35
- update = update?(mod)
36
- next unless update
36
+ next unless sync?(mod)
37
+ logger.debug "Syncing #{mod.name}"
37
38
  lockfile.sync(mod) # update (if version mismatch) or create (if missing)
38
39
  end
39
40
  end
40
41
 
41
- def update?(mod)
42
+ def sync?(mod)
42
43
  names = @options[:mods]
43
- return true if names.nil? || names.empty? # when empty never skip update
44
- names.include?(mod.name)
44
+ names.blank? or names.include?(mod.name)
45
45
  end
46
46
 
47
47
  def validate!
@@ -3,7 +3,6 @@ module TerraspaceBundler
3
3
  include Singleton
4
4
  extend Memoist
5
5
  include TB::Util::Logging
6
- include Dsl::Concern
7
6
 
8
7
  # dsl meta example:
9
8
  # {:global=>{:org=>"boltopspro"},
@@ -11,7 +10,7 @@ module TerraspaceBundler
11
10
  # [{:args=>["eks"], :options=>{:source=>"terraform-aws-eks"}},
12
11
  # {:args=>["vpc"], :options=>{:source=>"terraform-aws-vpc"}}]}
13
12
  def mods
14
- dsl.meta[:mods].map do |params|
13
+ TB.dsl.meta[:mods].map do |params|
15
14
  new_mod(params)
16
15
  end
17
16
  end
@@ -1,3 +1,3 @@
1
1
  module TerraspaceBundler
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -0,0 +1,3 @@
1
+ module "compute_instance" {
2
+ source = "../../../modules/compute_instance"
3
+ }
@@ -0,0 +1,3 @@
1
+ module "iam-user" {
2
+ source = "../../modules/iam-user"
3
+ }
@@ -0,0 +1,3 @@
1
+ module "consul" {
2
+ source="../.." # no leading space
3
+ }
@@ -0,0 +1,8 @@
1
+ module "consul" {
2
+ source = "hashicorp/consul/aws"
3
+ version = "0.1.0"
4
+ }
5
+
6
+ module "example" {
7
+ source = "github.com/hashicorp/example"
8
+ }
@@ -0,0 +1,3 @@
1
+ module "consul" {
2
+ source="../.." # no spaces
3
+ }
@@ -0,0 +1,3 @@
1
+ module "consul" {
2
+ source = "../.."
3
+ }
@@ -0,0 +1,3 @@
1
+ module "consul" {
2
+ source = "../.." # lots of extra spaces
3
+ }
@@ -0,0 +1,3 @@
1
+ module "consul" {
2
+ source = "../../"
3
+ }
@@ -22,6 +22,10 @@ module Helper
22
22
  def show_command?
23
23
  ENV['DEBUG'] || ENV['SHOW_COMMAND']
24
24
  end
25
+
26
+ def fixture(path)
27
+ "spec/fixtures/#{path}"
28
+ end
25
29
  end
26
30
 
27
31
  RSpec.configure do |c|
@@ -0,0 +1,86 @@
1
+ describe TB::Exporter::Stacks::Rewrite do
2
+ let(:rewrite) { described_class.new(stack) }
3
+ let(:stack) do
4
+ stack = double(:stack).as_null_object
5
+ allow(stack).to receive(:name).and_return("stack_name")
6
+ stack
7
+ end
8
+
9
+ before(:each) do
10
+ allow(IO).to receive(:write) do |path, text|
11
+ @text = text
12
+ end
13
+ end
14
+
15
+ context "has path-based module" do
16
+ let(:path) { fixture("rewrite/normal.tf") }
17
+ it "replace" do
18
+ rewrite.replace(path)
19
+ replaced = @text.include?('source = "../../modules/stack_name"')
20
+ expect(replaced).to be true
21
+ end
22
+ end
23
+
24
+ context "has path-based module with lots of extra spaces in between" do
25
+ let(:path) { fixture("rewrite/spaces.tf") }
26
+ it "replace" do
27
+ rewrite.replace(path)
28
+ replaced = @text.include?('source = "../../modules/stack_name"')
29
+ expect(replaced).to be true
30
+ end
31
+ end
32
+
33
+ context "has path-based module with lots of no spaces inbetween" do
34
+ let(:path) { fixture("rewrite/no-spaces.tf") }
35
+ it "replace" do
36
+ rewrite.replace(path)
37
+ replaced = @text.include?('source = "../../modules/stack_name"')
38
+ expect(replaced).to be true
39
+ end
40
+ end
41
+
42
+ context "has path-based module with no leading spaces" do
43
+ let(:path) { fixture("rewrite/no-leading.tf") }
44
+ it "replace" do
45
+ rewrite.replace(path)
46
+ replaced = @text.include?('source = "../../modules/stack_name"')
47
+ expect(replaced).to be true
48
+ end
49
+ end
50
+
51
+ context "module is within subfolder and not at root" do
52
+ let(:path) { fixture("rewrite/module-subfolder.tf") }
53
+ it "replace" do
54
+ rewrite.replace(path)
55
+ replaced = @text.include?('source = "../../modules/stack_name/modules/iam-user"')
56
+ expect(replaced).to be true
57
+ end
58
+ end
59
+
60
+ context "example is within subfolder of module examples" do
61
+ let(:path) { fixture("rewrite/example-subfolder.tf") }
62
+ it "replace" do
63
+ rewrite.replace(path)
64
+ replaced = @text.include?('source = "../../modules/stack_name/modules/compute_instance"')
65
+ expect(replaced).to be true
66
+ end
67
+ end
68
+
69
+ context "trailing slash" do
70
+ let(:path) { fixture("rewrite/trailing-slash.tf") }
71
+ it "replace" do
72
+ rewrite.replace(path)
73
+ replaced = @text.include?('source = "../../modules/stack_name"')
74
+ expect(replaced).to be true
75
+ end
76
+ end
77
+
78
+ context "not a path-based module" do
79
+ let(:path) { fixture("rewrite/no-path.tf") }
80
+ it "replace" do
81
+ rewrite.replace(path)
82
+ kept = @text.include?('source = "hashicorp/consul/aws"') # keeps current source link
83
+ expect(kept).to be true
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,75 @@
1
+ describe TB::Mod do
2
+ let(:mod) { described_class.new(props) }
3
+ let(:props) do
4
+ {
5
+ name: "example",
6
+ source: "org/terraform-aws-vpc",
7
+ }
8
+ end
9
+
10
+ context "normalize_stacks" do
11
+ context "single String" do
12
+ let(:stacks) { "vpc" }
13
+ it "return List of Hashes" do
14
+ options = mod.normalize_stacks(stacks)
15
+ expect(options).to eq(
16
+ [{:dest=>"app/stacks", :purge=>nil, :examples=>"examples", :src=>"vpc"}]
17
+ )
18
+ end
19
+ end
20
+
21
+ context "single Hash" do
22
+ let(:stacks) { {src: "vpc"} }
23
+ it "return List of Hashes" do
24
+ options = mod.normalize_stacks(stacks)
25
+ expect(options).to eq(
26
+ [{:dest=>"app/stacks", :purge=>nil, :examples=>"examples", :src=>"vpc"}]
27
+ )
28
+ end
29
+ end
30
+
31
+ context "multiple Strings" do
32
+ let(:stacks) { ["ec2", "vpc"] }
33
+ it "return List of Hashes" do
34
+ options = mod.normalize_stacks(stacks)
35
+ expect(options).to eq(
36
+ [{:dest=>"app/stacks", :purge=>nil, :examples=>"examples", :src=>"ec2"},
37
+ {:dest=>"app/stacks", :purge=>nil, :examples=>"examples", :src=>"vpc"}]
38
+ )
39
+ end
40
+ end
41
+
42
+ context "multiple Hash" do
43
+ let(:stacks) { [{src: "ec2"}, {src: "vpc"}] }
44
+ it "return List of Hashes" do
45
+ options = mod.normalize_stacks(stacks)
46
+ expect(options).to eq(
47
+ [{:dest=>"app/stacks", :purge=>nil, :examples=>"examples", :src=>"ec2"},
48
+ {:dest=>"app/stacks", :purge=>nil, :examples=>"examples", :src=>"vpc"}]
49
+ )
50
+ end
51
+ end
52
+
53
+ context "multiple Objects" do
54
+ let(:stacks) { ["ec2", {src: "vpc"}] }
55
+ it "return List of Hashes" do
56
+ options = mod.normalize_stacks(stacks)
57
+ expect(options).to eq(
58
+ [{:dest=>"app/stacks", :purge=>nil, :examples=>"examples", :src=>"ec2"},
59
+ {:dest=>"app/stacks", :purge=>nil, :examples=>"examples", :src=>"vpc"}]
60
+ )
61
+ end
62
+ end
63
+
64
+ context "multiple Objects with options" do
65
+ let(:stacks) { ["ec2", {src: "vpc", purge: true, dest: "vendor/stacks"}] }
66
+ it "return List of Hashes" do
67
+ options = mod.normalize_stacks(stacks)
68
+ expect(options).to eq(
69
+ [{:dest=>"app/stacks", :purge=>nil, :examples=>"examples", :src=>"ec2"},
70
+ {:dest=>"vendor/stacks", :purge=>true, :examples=>"examples", :src=>"vpc"}]
71
+ )
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,4 +1,4 @@
1
- describe TB::CLI::Runner do
1
+ describe TB::Runner do
2
2
  let(:runner) { described_class.new }
3
3
 
4
4
  context "runner" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terraspace-bundler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tung Nguyen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-14 00:00:00.000000000 Z
11
+ date: 2020-10-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -181,7 +181,6 @@ files:
181
181
  - lib/terraspace_bundler/cli/help/completion.md
182
182
  - lib/terraspace_bundler/cli/help/completion_script.md
183
183
  - lib/terraspace_bundler/cli/purge_cache.rb
184
- - lib/terraspace_bundler/cli/runner.rb
185
184
  - lib/terraspace_bundler/command.rb
186
185
  - lib/terraspace_bundler/completer.rb
187
186
  - lib/terraspace_bundler/completer/script.rb
@@ -189,11 +188,15 @@ files:
189
188
  - lib/terraspace_bundler/config.rb
190
189
  - lib/terraspace_bundler/core.rb
191
190
  - lib/terraspace_bundler/dsl.rb
192
- - lib/terraspace_bundler/dsl/concern.rb
193
191
  - lib/terraspace_bundler/dsl/syntax.rb
194
192
  - lib/terraspace_bundler/exporter.rb
193
+ - lib/terraspace_bundler/exporter/base.rb
194
+ - lib/terraspace_bundler/exporter/copy.rb
195
+ - lib/terraspace_bundler/exporter/stacks.rb
196
+ - lib/terraspace_bundler/exporter/stacks/base.rb
197
+ - lib/terraspace_bundler/exporter/stacks/rewrite.rb
198
+ - lib/terraspace_bundler/exporter/stacks/stack.rb
195
199
  - lib/terraspace_bundler/info.rb
196
- - lib/terraspace_bundler/installer.rb
197
200
  - lib/terraspace_bundler/list.rb
198
201
  - lib/terraspace_bundler/lockfile.rb
199
202
  - lib/terraspace_bundler/lockfile/version_comparer.rb
@@ -206,13 +209,25 @@ files:
206
209
  - lib/terraspace_bundler/mod/props_builder.rb
207
210
  - lib/terraspace_bundler/mod/props_extension.rb
208
211
  - lib/terraspace_bundler/mod/registry.rb
212
+ - lib/terraspace_bundler/mod/stack_concern.rb
213
+ - lib/terraspace_bundler/runner.rb
209
214
  - lib/terraspace_bundler/syncer.rb
210
215
  - lib/terraspace_bundler/terrafile.rb
211
216
  - lib/terraspace_bundler/util/git.rb
212
217
  - lib/terraspace_bundler/util/logging.rb
213
218
  - lib/terraspace_bundler/version.rb
214
219
  - spec/fixtures/Terrafile
220
+ - spec/fixtures/rewrite/example-subfolder.tf
221
+ - spec/fixtures/rewrite/module-subfolder.tf
222
+ - spec/fixtures/rewrite/no-leading.tf
223
+ - spec/fixtures/rewrite/no-path.tf
224
+ - spec/fixtures/rewrite/no-spaces.tf
225
+ - spec/fixtures/rewrite/normal.tf
226
+ - spec/fixtures/rewrite/spaces.tf
227
+ - spec/fixtures/rewrite/trailing-slash.tf
215
228
  - spec/spec_helper.rb
229
+ - spec/terraform_bundler/exporter/stacks/rewrite_spec.rb
230
+ - spec/terraform_bundler/mod_spec.rb
216
231
  - spec/terraform_bundler/runner_spec.rb
217
232
  - terraspace-bundler.gemspec
218
233
  homepage: https://github.com/boltops-tools/terraspace-bundler
@@ -240,5 +255,15 @@ specification_version: 4
240
255
  summary: Bundles terraform modules
241
256
  test_files:
242
257
  - spec/fixtures/Terrafile
258
+ - spec/fixtures/rewrite/example-subfolder.tf
259
+ - spec/fixtures/rewrite/module-subfolder.tf
260
+ - spec/fixtures/rewrite/no-leading.tf
261
+ - spec/fixtures/rewrite/no-path.tf
262
+ - spec/fixtures/rewrite/no-spaces.tf
263
+ - spec/fixtures/rewrite/normal.tf
264
+ - spec/fixtures/rewrite/spaces.tf
265
+ - spec/fixtures/rewrite/trailing-slash.tf
243
266
  - spec/spec_helper.rb
267
+ - spec/terraform_bundler/exporter/stacks/rewrite_spec.rb
268
+ - spec/terraform_bundler/mod_spec.rb
244
269
  - spec/terraform_bundler/runner_spec.rb
@@ -1,7 +0,0 @@
1
- class TerraspaceBundler::CLI
2
- class Runner < Base
3
- def run
4
- TB::Installer.new(@options).run
5
- end
6
- end
7
- end
@@ -1,7 +0,0 @@
1
- class TerraspaceBundler::Dsl
2
- module Concern
3
- def dsl
4
- TB.dsl
5
- end
6
- end
7
- end