terraspace-bundler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +19 -0
  7. data/LICENSE.txt +201 -0
  8. data/README.md +82 -0
  9. data/Rakefile +6 -0
  10. data/exe/terraform-bundler +14 -0
  11. data/lib/terraspace-bundler.rb +1 -0
  12. data/lib/terraspace_bundler.rb +23 -0
  13. data/lib/terraspace_bundler/autoloader.rb +22 -0
  14. data/lib/terraspace_bundler/cli.rb +24 -0
  15. data/lib/terraspace_bundler/cli/base.rb +9 -0
  16. data/lib/terraspace_bundler/cli/bundle.rb +27 -0
  17. data/lib/terraspace_bundler/cli/clean.rb +11 -0
  18. data/lib/terraspace_bundler/cli/help.rb +9 -0
  19. data/lib/terraspace_bundler/cli/help/bundle/install.md +3 -0
  20. data/lib/terraspace_bundler/cli/help/completion.md +20 -0
  21. data/lib/terraspace_bundler/cli/help/completion_script.md +3 -0
  22. data/lib/terraspace_bundler/cli/install.rb +7 -0
  23. data/lib/terraspace_bundler/cli/update.rb +7 -0
  24. data/lib/terraspace_bundler/command.rb +89 -0
  25. data/lib/terraspace_bundler/completer.rb +159 -0
  26. data/lib/terraspace_bundler/completer/script.rb +6 -0
  27. data/lib/terraspace_bundler/completer/script.sh +10 -0
  28. data/lib/terraspace_bundler/config.rb +15 -0
  29. data/lib/terraspace_bundler/core.rb +21 -0
  30. data/lib/terraspace_bundler/dsl.rb +18 -0
  31. data/lib/terraspace_bundler/dsl/syntax.rb +20 -0
  32. data/lib/terraspace_bundler/helper/git.rb +21 -0
  33. data/lib/terraspace_bundler/installer.rb +45 -0
  34. data/lib/terraspace_bundler/logger.rb +26 -0
  35. data/lib/terraspace_bundler/logging.rb +7 -0
  36. data/lib/terraspace_bundler/mod.rb +85 -0
  37. data/lib/terraspace_bundler/mod/export.rb +23 -0
  38. data/lib/terraspace_bundler/mod/locked.rb +48 -0
  39. data/lib/terraspace_bundler/mod/registry.rb +72 -0
  40. data/lib/terraspace_bundler/mod/sync.rb +53 -0
  41. data/lib/terraspace_bundler/mod/tmp_paths.rb +28 -0
  42. data/lib/terraspace_bundler/setup.rb +20 -0
  43. data/lib/terraspace_bundler/syncer.rb +13 -0
  44. data/lib/terraspace_bundler/updater.rb +49 -0
  45. data/lib/terraspace_bundler/updater/lockfile.rb +48 -0
  46. data/lib/terraspace_bundler/util/registry.rb +24 -0
  47. data/lib/terraspace_bundler/version.rb +3 -0
  48. data/spec/fixtures/Terrafile +6 -0
  49. data/spec/spec_helper.rb +29 -0
  50. data/spec/terraform_bundler/installer_spec.rb +16 -0
  51. data/terraspace-bundler.gemspec +32 -0
  52. metadata +237 -0
@@ -0,0 +1,21 @@
1
+ module TerraspaceBundler::Helper
2
+ module Git
3
+ include TB::Logging
4
+
5
+ def sh(command)
6
+ command = "#{command} 2>&1" # always need output for the sha
7
+ logger.debug "=> #{command}"
8
+ out = `#{command}`
9
+ unless $?.success?
10
+ logger.error "ERROR: running #{command}"
11
+ logger.error out
12
+ exit $?.exitstatus
13
+ end
14
+ out
15
+ end
16
+
17
+ def git(command)
18
+ sh("git #{command}")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,45 @@
1
+ module TerraspaceBundler
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
+ def run
11
+ Setup.new(@options).setup!
12
+ download
13
+ export
14
+ logger.info "Modules saved to #{TB.config.export_path}"
15
+ end
16
+
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
24
+ end
25
+
26
+ Syncer.new(mods).run
27
+ end
28
+
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
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,26 @@
1
+ require 'logger'
2
+
3
+ module TerraspaceBundler
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
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ module TerraspaceBundler
2
+ module Logging
3
+ def logger
4
+ TerraspaceBundler.logger
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,85 @@
1
+ module TerraspaceBundler
2
+ class Mod
3
+ extend Memoist
4
+ include TB::Util::Registry
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
25
+ end
26
+
27
+ def full_org
28
+ normalize_git_url(org)
29
+ end
30
+
31
+ def org
32
+ obtain_org(@options[:source], @global[:org])
33
+ end
34
+
35
+ def url
36
+ normalize_url(source)
37
+ end
38
+
39
+ def normalize_url(source)
40
+ return registry_github if registry?(source)
41
+ normalize_git_url(source)
42
+ end
43
+
44
+ def path
45
+ @options[:path]
46
+ end
47
+
48
+ def name
49
+ @args.first
50
+ end
51
+
52
+ def version
53
+ @version || @ref || @tag || @branch
54
+ end
55
+
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@")
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,23 @@
1
+ class TerraspaceBundler::Mod
2
+ class Export
3
+ include TmpPaths
4
+
5
+ def initialize(locked_mod)
6
+ @locked_mod = locked_mod
7
+ end
8
+
9
+ def run
10
+ stage_path = stage_path(@locked_mod.full_repo)
11
+ stage_path = "#{stage_path}/#{@locked_mod.path}" if @locked_mod.path
12
+ export_path = export_path(@locked_mod.name)
13
+ FileUtils.rm_rf(export_path)
14
+ FileUtils.mkdir_p(File.dirname(export_path))
15
+ FileUtils.cp_r(stage_path, export_path)
16
+ FileUtils.rm_rf("#{export_path}/.git")
17
+ end
18
+
19
+ def export_path(repo)
20
+ "#{TB.config.export_path}/#{repo}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,48 @@
1
+ class TerraspaceBundler::Mod
2
+ class Locked
3
+ include TB::Util::Registry
4
+
5
+ attr_reader :name, :path, :source, :version
6
+ def initialize(name, info)
7
+ @name, @info = name.to_s, info
8
+
9
+ @path = @info[:path]
10
+ @version = @info[:version]
11
+ end
12
+
13
+ def source
14
+ @info[:source]
15
+ end
16
+
17
+ def org
18
+ obtain_org(source)
19
+ end
20
+
21
+ def repo
22
+ s = registry?(source) ? registry_github : source
23
+ File.basename(s)
24
+ end
25
+
26
+ def full_repo
27
+ "#{org}/#{repo}"
28
+ end
29
+
30
+ def to_mod
31
+ # copy all props in Terrafile.lock
32
+ options = @info.clone
33
+ # add org
34
+ options.merge!(org: @org)
35
+
36
+ data = {
37
+ args: @name,
38
+ options: options,
39
+ }
40
+ TB::Mod.new(data) # no need for global, info in the lockfile is enough
41
+ end
42
+
43
+ def export
44
+ export = Export.new(self)
45
+ export.run
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,72 @@
1
+ require 'net/http'
2
+ require 'open-uri'
3
+
4
+ class TerraspaceBundler::Mod
5
+ class Registry
6
+ def initialize(mod)
7
+ @mod = mod
8
+ end
9
+
10
+ # Terrafile example
11
+ #
12
+ # mod "sg", source: "terraform-aws-modules/security-group/aws", version: "3.10.0"
13
+ #
14
+ # Resources: https://www.terraform.io/docs/registry/api.html
15
+ #
16
+ # Latest version:
17
+ #
18
+ # https://registry.terraform.io/v1/modules/terraform-aws-modules/sqs/aws/2.1.0/download
19
+ #
20
+ # The latest version returns an 302 and contains a location header that is followed
21
+ # and then downloaded the same way the specific version is downloaded.
22
+ #
23
+ # Specific version:
24
+ #
25
+ # https://registry.terraform.io/v1/modules/terraform-aws-modules/sqs/aws/download
26
+ #
27
+ # The specific version returns an 204 and then we grab the download url info form the x-terraform-get header.
28
+ def to_github
29
+ base_site = "https://registry.terraform.io"
30
+ base_url = "#{base_site}/v1/modules"
31
+
32
+ version = @mod.version.sub(/^v/,'') if @mod.version # v1.0 => 1.0
33
+ api_url = [base_url, @mod.source, version, "download"].compact.join('/')
34
+ resp = http_request(api_url)
35
+
36
+ case resp.code.to_i
37
+ when 204
38
+ download_url = resp.header["x-terraform-get"]
39
+ when 302
40
+ next_url = "#{base_site}#{resp.header["location"]}"
41
+ resp = http_request(next_url)
42
+ download_url = resp.header["x-terraform-get"]
43
+ else
44
+ raise "Unable to lookup up module in Terraform Registry: #{resp}"
45
+ end
46
+
47
+ download_url.sub(%r{/archive/.*},'')
48
+ end
49
+
50
+ private
51
+ def http_request(url)
52
+ uri = URI(url)
53
+ http = Net::HTTP.new(uri.host, uri.port)
54
+ http.use_ssl = uri.scheme == "https"
55
+ # Total time will be 40s = 20 x 2
56
+ http.max_retries = 1 # Default is already 1, just being explicit
57
+ http.read_timeout = 20 # Sites that dont return in 20 seconds are considered down
58
+ request = Net::HTTP::Get.new(uri)
59
+ begin
60
+ http.request(request) # response
61
+ rescue Net::OpenTimeout => e # internal ELB but VPC is not configured for Lambda function
62
+ http_request_error_message(e)
63
+ puts "The Lambda Function does not seem to have network access to the url. It might not be configured with a VpcConfig. Please double check that the related vpc variables are configured: @subnet_ids, @vpc_id, @security_group_ingress"
64
+ rescue Exception => e
65
+ # Net::ReadTimeout - too slow
66
+ # Errno::ECONNREFUSED - completely down
67
+ # SocketError - improper url "dsfjsl" instead of example.com
68
+ http_request_error_message(e)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,53 @@
1
+ class TerraspaceBundler::Mod
2
+ class Sync
3
+ extend Memoist
4
+ include TB::Helper::Git
5
+ include TB::Logging
6
+ include TB::Mod::TmpPaths
7
+
8
+ attr_reader :sha
9
+ def initialize(mod, url)
10
+ @mod, @url = mod, url
11
+ end
12
+
13
+ def run
14
+ setup_tmp
15
+ org_path = "#{cache_root}/#{@mod.org}"
16
+ FileUtils.mkdir_p(org_path)
17
+ Dir.chdir(org_path) do
18
+ name = File.basename(@url).sub('.git','')
19
+ unless File.exist?(name)
20
+ sh "git clone #{@url}"
21
+ end
22
+
23
+ Dir.chdir(name) do
24
+ git "pull"
25
+ git "submodule update --init"
26
+ stage(name)
27
+ end
28
+ end
29
+ end
30
+
31
+ def stage(name)
32
+ copy_to_stage(name)
33
+ switch_to_specific_version(name)
34
+ end
35
+
36
+ def switch_to_specific_version(name)
37
+ stage_path = stage_path("#{@mod.org}/#{name}")
38
+ logger.debug "Within: #{stage_path}"
39
+ Dir.chdir(stage_path) do
40
+ git "checkout #{@mod.checkout_version}" if @mod.checkout_version
41
+ @sha = git("rev-parse HEAD").strip
42
+ end
43
+ end
44
+
45
+ def copy_to_stage(name)
46
+ cache_path = cache_path("#{@mod.org}/#{name}")
47
+ stage_path = stage_path("#{@mod.org}/#{name}")
48
+ FileUtils.rm_rf(stage_path)
49
+ FileUtils.mkdir_p(File.dirname(stage_path))
50
+ FileUtils.cp_r(cache_path, stage_path)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,28 @@
1
+ class TerraspaceBundler::Mod
2
+ module TmpPaths
3
+ def setup_tmp
4
+ FileUtils.mkdir_p(cache_root)
5
+ FileUtils.mkdir_p(stage_root)
6
+ end
7
+
8
+ def tmp_root
9
+ "/tmp/terraspace/bundler"
10
+ end
11
+
12
+ def cache_root
13
+ "#{tmp_root}/cache"
14
+ end
15
+
16
+ def stage_root
17
+ "#{tmp_root}/stage"
18
+ end
19
+
20
+ def cache_path(name)
21
+ "#{cache_root}/#{name}"
22
+ end
23
+
24
+ def stage_path(name)
25
+ "#{stage_root}/#{name}"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ module TerraspaceBundler
2
+ class Setup
3
+ def initialize(options={})
4
+ @options = options
5
+ end
6
+
7
+ def setup!
8
+ # Run the DSL to possibly override the config defaults
9
+ meta = Dsl.new.run
10
+ export_path = meta[:global][:export_path]
11
+ TB.config.export_path = export_path if export_path
12
+
13
+ # cli --terrafile option
14
+ if @options[:terrafile]
15
+ TB.config.terrafile = @options[:terrafile]
16
+ TB.config.lockfile = "#{@options[:terrafile]}.lock"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module TerraspaceBundler
2
+ class Syncer
3
+ def initialize(mods)
4
+ @mods = mods
5
+ end
6
+
7
+ def run
8
+ @mods.each do |mod|
9
+ mod.sync
10
+ end
11
+ end
12
+ end
13
+ end