terraspace-bundler 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +72 -24
  4. data/lib/terraspace_bundler.rb +1 -0
  5. data/lib/terraspace_bundler/cli/base.rb +9 -1
  6. data/lib/terraspace_bundler/cli/bundle.rb +26 -12
  7. data/lib/terraspace_bundler/cli/help/bundle/info.md +8 -0
  8. data/lib/terraspace_bundler/cli/help/bundle/install.md +1 -1
  9. data/lib/terraspace_bundler/cli/help/bundle/list.md +9 -0
  10. data/lib/terraspace_bundler/cli/help/bundle/update.md +6 -0
  11. data/lib/terraspace_bundler/cli/{clean.rb → purge_cache.rb} +3 -3
  12. data/lib/terraspace_bundler/cli/{install.rb → runner.rb} +1 -1
  13. data/lib/terraspace_bundler/config.rb +15 -2
  14. data/lib/terraspace_bundler/core.rb +9 -4
  15. data/lib/terraspace_bundler/dsl.rb +6 -3
  16. data/lib/terraspace_bundler/dsl/concern.rb +7 -0
  17. data/lib/terraspace_bundler/dsl/syntax.rb +8 -0
  18. data/lib/terraspace_bundler/exporter.rb +47 -0
  19. data/lib/terraspace_bundler/info.rb +25 -0
  20. data/lib/terraspace_bundler/installer.rb +15 -33
  21. data/lib/terraspace_bundler/list.rb +22 -0
  22. data/lib/terraspace_bundler/lockfile.rb +70 -0
  23. data/lib/terraspace_bundler/lockfile/version_comparer.rb +43 -0
  24. data/lib/terraspace_bundler/lockfile/yamler.rb +37 -0
  25. data/lib/terraspace_bundler/logger.rb +0 -20
  26. data/lib/terraspace_bundler/logger/formatter.rb +7 -0
  27. data/lib/terraspace_bundler/mod.rb +31 -63
  28. data/lib/terraspace_bundler/mod/downloader.rb +55 -0
  29. data/lib/terraspace_bundler/mod/{tmp_paths.rb → path_concern.rb} +1 -1
  30. data/lib/terraspace_bundler/mod/props_builder.rb +57 -0
  31. data/lib/terraspace_bundler/mod/props_extension.rb +15 -0
  32. data/lib/terraspace_bundler/mod/registry.rb +8 -6
  33. data/lib/terraspace_bundler/syncer.rb +62 -4
  34. data/lib/terraspace_bundler/terrafile.rb +39 -0
  35. data/lib/terraspace_bundler/util/git.rb +31 -0
  36. data/lib/terraspace_bundler/{logging.rb → util/logging.rb} +1 -1
  37. data/lib/terraspace_bundler/version.rb +1 -1
  38. data/spec/fixtures/Terrafile +2 -2
  39. data/spec/terraform_bundler/runner_spec.rb +17 -0
  40. metadata +24 -17
  41. data/lib/terraspace_bundler/cli/update.rb +0 -7
  42. data/lib/terraspace_bundler/helper/git.rb +0 -21
  43. data/lib/terraspace_bundler/mod/export.rb +0 -23
  44. data/lib/terraspace_bundler/mod/locked.rb +0 -48
  45. data/lib/terraspace_bundler/mod/sync.rb +0 -53
  46. data/lib/terraspace_bundler/setup.rb +0 -20
  47. data/lib/terraspace_bundler/updater.rb +0 -49
  48. data/lib/terraspace_bundler/updater/lockfile.rb +0 -48
  49. data/lib/terraspace_bundler/util/registry.rb +0 -24
  50. data/spec/terraform_bundler/installer_spec.rb +0 -16
@@ -0,0 +1,25 @@
1
+ module TerraspaceBundler
2
+ class Info < TB::CLI::Base
3
+ def run
4
+ name = @options[:mod]
5
+ found = lockfile.mods.find { |m| m.name == @options[:mod] }
6
+ if found
7
+ show(found)
8
+ else
9
+ logger.info "Could not find module in #{TB.config.lockfile}: #{name}".color(:red)
10
+ end
11
+ end
12
+
13
+ def show(mod)
14
+ props = mod.props.reject { |k,v| k == :name }.stringify_keys # for sort
15
+ puts "#{mod.name}:"
16
+ props.keys.sort.each do |k|
17
+ puts " #{k}: #{props[k]}"
18
+ end
19
+ end
20
+
21
+ def lockfile
22
+ Lockfile.instance
23
+ end
24
+ end
25
+ end
@@ -1,45 +1,27 @@
1
1
  module TerraspaceBundler
2
2
  class Installer < CLI::Base
3
- extend Memoist
4
-
5
- def initialize(options={})
6
- super
7
- @download = options[:download].nil? ? true : options[:download]
8
- end
9
-
10
3
  def run
11
- Setup.new(@options).setup!
12
- download
13
- export
14
- logger.info "Modules saved to #{TB.config.export_path}"
4
+ Syncer.new(@options).run
5
+ Exporter.new(@options).run
6
+ finish_message
15
7
  end
16
8
 
17
- def download
18
- return unless @download
19
-
20
- if File.exist?(TB.config.lockfile)
21
- puts "Bundling modules from #{TB.config.lockfile}..."
22
- else
23
- Updater.new(@options).run(without_install: true) # creates Terrafile.lock
9
+ def finish_message
10
+ no_modules_found = true
11
+ export_paths.each do |export_path|
12
+ found = Dir.exist?(export_path) && !Dir.empty?(export_path)
13
+ next unless found
14
+ logger.info "Modules saved to #{export_path}"
15
+ no_modules_found = false
24
16
  end
25
17
 
26
- Syncer.new(mods).run
18
+ logger.info("No modules were found.") if no_modules_found
27
19
  end
28
20
 
29
- def export
30
- locked_mods.each(&:export)
31
- end
32
-
33
- def mods
34
- locked_mods.map(&:to_mod)
35
- end
36
- memoize :mods
37
-
38
- def locked_mods
39
- data = YAML.load_file(TB.config.lockfile).deep_symbolize_keys
40
- data.map do |name, info|
41
- Mod::Locked.new(name, info)
42
- end
21
+ def export_paths
22
+ export_paths = Terrafile.instance.mods.map(&:export_to).compact.uniq
23
+ export_paths << TB.config.export_path
24
+ export_paths
43
25
  end
44
26
  end
45
27
  end
@@ -0,0 +1,22 @@
1
+ module TerraspaceBundler
2
+ class List < TB::CLI::Base
3
+ def run
4
+ file = TB.config.lockfile
5
+ unless File.exist?(file)
6
+ logger.info "No #{file} found".color(:red)
7
+ logger.info "Maybe run: terraspace bundle"
8
+ return
9
+ end
10
+
11
+ logger.info "Modules included by #{file}\n\n"
12
+ lockfile.mods.each do |mod|
13
+ logger.info " #{mod.name}"
14
+ end
15
+ logger.info "\nUse `terraspace bundle info` to print more detailed information about a module"
16
+ end
17
+
18
+ def lockfile
19
+ Lockfile.instance
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,70 @@
1
+ require "yaml"
2
+
3
+ module TerraspaceBundler
4
+ class Lockfile
5
+ include Singleton
6
+ include TB::Util::Logging
7
+
8
+ # {"vpc"=>
9
+ # {"sha"=>"52328b2b5197f9b95e3005cfcfb99595040ee45b",
10
+ # "source"=>"org/terraform-aws-vpc",
11
+ # "url"=>"git@github.com:org/terraform-aws-vpc"},
12
+ # "instance"=>
13
+ # {"sha"=>"570cca3ea7b25e3af1961dc57b27ca2c129b934a",
14
+ # "source"=>"org/terraform-aws-instance",
15
+ # "url"=>"git@github.com:org/terraform-aws-instance"}}
16
+ @@mods = nil
17
+ def mods
18
+ return @@mods if @@mods
19
+ lockfile = TB.config.lockfile
20
+ mods = File.exist?(lockfile) ? YAML.load_file(lockfile) : []
21
+ @@mods = mods.map do |name, props|
22
+ new_mod(name, props)
23
+ end
24
+ end
25
+
26
+ def new_mod(name, props)
27
+ Mod.new(props.merge(name: name))
28
+ end
29
+
30
+ # update (if version mismatch) or create (if missing)
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
35
+ end
36
+
37
+ # mod built from Terrafile
38
+ def changed?(mod)
39
+ # missing module case
40
+ found = mods.find { |locked_mod| locked_mod.name == mod.name }
41
+ unless found
42
+ logger.debug "Replacing mod: #{mod.name}. Not found in Terrafile.lock"
43
+ return true
44
+ end
45
+
46
+ comparer = VersionComparer.new(found, mod)
47
+ comparer.run
48
+ logger.debug(comparer.reason) if comparer.reason
49
+ comparer.changed?
50
+ end
51
+
52
+ def replace!(mod)
53
+ # mods are immediately fresh from writing to @@mods directly
54
+ @@mods.delete_if { |m| m.name == mod.name }
55
+ @@mods << mod
56
+ @@mods.sort_by!(&:name)
57
+ end
58
+
59
+ def prune(removed_mods)
60
+ removals = removed_mods.map(&:name)
61
+ @@mods.delete_if { |m| removals.include?(m.name) }
62
+ end
63
+
64
+ class << self
65
+ def write
66
+ Yamler.write(@@mods) if @@mods
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,43 @@
1
+ class TerraspaceBundler::Lockfile
2
+ class VersionComparer
3
+ attr_reader :reason, :changed
4
+ def initialize(locked, current)
5
+ @locked, @current = locked, current
6
+ @changed = false
7
+ end
8
+
9
+ def changed?
10
+ @changed
11
+ end
12
+
13
+ def run
14
+ @changed = false
15
+ strict_versions = %w[subfolder ref tag export_to]
16
+ strict_versions.each do |version|
17
+ @changed = @locked.send(version) != @current.send(version)
18
+ if @changed
19
+ @reason = reason_message(version)
20
+ return @changed
21
+ end
22
+ end
23
+
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
34
+ end
35
+
36
+ @changed
37
+ end
38
+
39
+ def reason_message(version)
40
+ "Replacing mod: #{@current.name}. #{version} is different in Terrafile and Terrafile.lock"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,37 @@
1
+ require "yaml"
2
+
3
+ class TerraspaceBundler::Lockfile
4
+ class Yamler
5
+ def initialize(mods)
6
+ @mods = mods.sort_by(&:name)
7
+ end
8
+
9
+ def dump
10
+ YAML.dump(data.deep_stringify_keys)
11
+ end
12
+
13
+ def data
14
+ @mods.inject({}) do |acc, mod|
15
+ acc.merge(item(mod))
16
+ end
17
+ end
18
+
19
+ def item(mod)
20
+ props = mod.props.dup # passthrough: name, url, version, ref, tag, branch etc
21
+ props.delete(:name) # different structure in Terrafile.lock YAML
22
+ props[:sha] = mod.sha
23
+ props.delete_if { |k,v| v.nil? }
24
+ { mod.name => props }
25
+ end
26
+
27
+ def write
28
+ IO.write(TB.config.lockfile, dump)
29
+ end
30
+
31
+ class << self
32
+ def write(mods)
33
+ new(mods).write
34
+ end
35
+ end
36
+ end
37
+ end
@@ -2,25 +2,5 @@ require 'logger'
2
2
 
3
3
  module TerraspaceBundler
4
4
  class Logger < ::Logger
5
- # Only need to override the add method as the other calls all lead to it.
6
- def add(severity, message = nil, progname = nil)
7
- # Taken from Logger#add source
8
- # https://ruby-doc.org/stdlib-2.5.1/libdoc/logger/rdoc/Logger.html#method-i-add
9
- if message.nil?
10
- if block_given?
11
- message = yield
12
- else
13
- message = progname
14
- progname = @progname
15
- end
16
- end
17
-
18
- super # original logic
19
- end
20
-
21
- # plain formatting
22
- def format_message(severity, timestamp, progname, msg)
23
- "#{msg}\n"
24
- end
25
5
  end
26
6
  end
@@ -0,0 +1,7 @@
1
+ class TerraspaceBundler::Logger
2
+ class Formatter < ::Logger::Formatter
3
+ def call(severity, time, progname, msg)
4
+ msg =~ /\n$/ ? msg : "#{msg}\n"
5
+ end
6
+ end
7
+ end
@@ -1,85 +1,53 @@
1
1
  module TerraspaceBundler
2
2
  class Mod
3
- extend Memoist
4
- include TB::Util::Registry
3
+ extend PropsExtension
4
+ props :name, :source, :url, :subfolder, :type, :export_to
5
5
 
6
- attr_reader :branch, :ref, :tag, :sha, :source
7
- def initialize(data={}, global={})
8
- @data, @global = data, global
9
- @args = data[:args]
10
- @options = data[:options]
11
-
12
- # support variety of options, prefer version
13
- @version = @options[:version]
14
- @branch = @options[:branch]
15
- @ref = @options[:ref]
16
- @tag = @options[:tag]
17
- end
18
-
19
- def source
20
- if @options[:source].split('/').size == 1
21
- "#{org}/#{@options[:source]}"
22
- else
23
- @options[:source]
24
- end
6
+ attr_reader :props, :version, :ref, :tag, :branch
7
+ def initialize(props={})
8
+ @props = props.symbolize_keys
9
+ # These props are used for version comparing by VersionComparer
10
+ @version, @ref, @tag, @branch = @props[:version], @props[:ref], @props[:tag], @props[:branch]
25
11
  end
26
12
 
27
- def full_org
28
- normalize_git_url(org)
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
29
17
  end
30
18
 
31
- def org
32
- obtain_org(@options[:source], @global[:org])
19
+ def checkout_version
20
+ v = detected_version
21
+ v = "v#{v}" if type == "registry" && @version && !v.starts_with?("v")
22
+ v
33
23
  end
34
24
 
35
- def url
36
- normalize_url(source)
25
+ # use url instead of source because for registry modules, the repo name is different
26
+ def repo
27
+ url_words[-1]
37
28
  end
38
29
 
39
- def normalize_url(source)
40
- return registry_github if registry?(source)
41
- normalize_git_url(source)
30
+ def org
31
+ url_words[-2] # second to last word
42
32
  end
43
33
 
44
- def path
45
- @options[:path]
34
+ def full_repo
35
+ "#{org}/#{repo}"
46
36
  end
47
37
 
48
- def name
49
- @args.first
38
+ private
39
+ def fetch_sha
40
+ downloader = Downloader.new(self)
41
+ downloader.run
42
+ downloader.sha
50
43
  end
51
-
52
- def version
44
+ # support variety of options, prefer version
45
+ def detected_version
53
46
  @version || @ref || @tag || @branch
54
47
  end
55
48
 
56
- def checkout_version
57
- v = version
58
- v = "v#{v}" if registry?(source) && @version && !v.starts_with?("v")
59
- v
60
- end
61
-
62
- def sync
63
- sync = Sync.new(self, url)
64
- sync.run
65
- @sha = sync.sha
66
- end
67
- memoize :sync
68
-
69
- def normalize_git_url(s)
70
- if git_url?(s)
71
- s
72
- else
73
- "#{base_url}#{s}"
74
- end
75
- end
76
-
77
- def base_url
78
- @global[:base_url] || "git@github.com:"
79
- end
80
-
81
- def git_url?(s)
82
- s.include?("http") || s.include?("git@")
49
+ def url_words
50
+ url.split('/')
83
51
  end
84
52
  end
85
53
  end
@@ -0,0 +1,55 @@
1
+ class TerraspaceBundler::Mod
2
+ class Downloader
3
+ extend Memoist
4
+ include TB::Util::Git
5
+ include TB::Util::Logging
6
+ include TB::Mod::PathConcern
7
+
8
+ attr_reader :sha
9
+ def initialize(mod)
10
+ @mod = mod
11
+ end
12
+
13
+ def run
14
+ setup_tmp
15
+ org_path = "#{cache_root}/#{@mod.org}"
16
+ FileUtils.mkdir_p(org_path)
17
+
18
+ Dir.chdir(org_path) do
19
+ unless File.exist?(@mod.repo)
20
+ sh "git clone #{@mod.url}"
21
+ end
22
+
23
+ Dir.chdir(@mod.repo) do
24
+ git "pull"
25
+ git "submodule update --init"
26
+ stage
27
+ end
28
+ end
29
+ end
30
+
31
+ def stage
32
+ copy_to_stage
33
+ # TODO: if there's no master, need to checkout if its the default branch
34
+ checkout = @mod.checkout_version || "master"
35
+ switch_version(checkout)
36
+ end
37
+
38
+ def switch_version(version)
39
+ stage_path = stage_path("#{@mod.org}/#{@mod.repo}")
40
+ logger.debug "Within: #{stage_path}"
41
+ Dir.chdir(stage_path) do
42
+ git "checkout #{version}"
43
+ @sha = git("rev-parse HEAD").strip
44
+ end
45
+ end
46
+
47
+ def copy_to_stage
48
+ cache_path = cache_path("#{@mod.org}/#{@mod.repo}")
49
+ stage_path = stage_path("#{@mod.org}/#{@mod.repo}")
50
+ FileUtils.rm_rf(stage_path)
51
+ FileUtils.mkdir_p(File.dirname(stage_path))
52
+ FileUtils.cp_r(cache_path, stage_path)
53
+ end
54
+ end
55
+ end