terraspace 0.1.1 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -2
  3. data/README.md +20 -12
  4. data/lib/templates/base/git_hook/hook.sh +5 -0
  5. data/lib/templates/base/project/.gitignore +0 -1
  6. data/lib/templates/base/shim/terraspace +7 -0
  7. data/lib/templates/hcl/project/config/terraform/backend.tf.tt +3 -3
  8. data/lib/templates/plugin/lib/templates/hcl/project/config/terraform/backend.tf.tt +2 -2
  9. data/lib/terraspace/app.rb +8 -0
  10. data/lib/terraspace/builder.rb +6 -45
  11. data/lib/terraspace/cli.rb +34 -18
  12. data/lib/terraspace/cli/build/placeholder.rb +40 -0
  13. data/lib/terraspace/cli/cloud.rb +24 -0
  14. data/lib/terraspace/cli/commander.rb +8 -1
  15. data/lib/terraspace/cli/init.rb +67 -0
  16. data/lib/terraspace/cli/list.rb +13 -0
  17. data/lib/terraspace/cli/new.rb +8 -0
  18. data/lib/terraspace/cli/new/git_hook.rb +33 -0
  19. data/lib/terraspace/cli/new/shim.rb +58 -0
  20. data/lib/terraspace/cli/summary.rb +9 -12
  21. data/lib/terraspace/compiler/backend.rb +9 -37
  22. data/lib/terraspace/compiler/backend/parser.rb +42 -0
  23. data/lib/terraspace/compiler/builder.rb +6 -2
  24. data/lib/terraspace/compiler/cleaner.rb +19 -2
  25. data/lib/terraspace/compiler/cleaner/backend_change.rb +1 -1
  26. data/lib/terraspace/compiler/dsl/syntax/mod.rb +1 -0
  27. data/lib/terraspace/compiler/dsl/syntax/mod/backend.rb +16 -3
  28. data/lib/terraspace/compiler/expander.rb +28 -1
  29. data/lib/terraspace/compiler/writer.rb +1 -1
  30. data/lib/terraspace/core.rb +7 -1
  31. data/lib/terraspace/mod.rb +37 -12
  32. data/lib/terraspace/mod/remote.rb +1 -1
  33. data/lib/terraspace/plugin/expander/interface.rb +48 -5
  34. data/lib/terraspace/plugin/infer_provider.rb +15 -0
  35. data/lib/terraspace/plugin/layer/interface.rb +5 -0
  36. data/lib/terraspace/plugin/summary/interface.rb +1 -0
  37. data/lib/terraspace/seeder.rb +4 -4
  38. data/lib/terraspace/terraform/api.rb +58 -0
  39. data/lib/terraspace/terraform/api/client.rb +10 -0
  40. data/lib/terraspace/terraform/api/http.rb +106 -0
  41. data/lib/terraspace/terraform/api/var.rb +72 -0
  42. data/lib/terraspace/terraform/api/vars.rb +38 -0
  43. data/lib/terraspace/terraform/api/vars/base.rb +7 -0
  44. data/lib/terraspace/terraform/api/vars/json.rb +14 -0
  45. data/lib/terraspace/terraform/api/vars/rb.rb +21 -0
  46. data/lib/terraspace/terraform/args/custom.rb +1 -1
  47. data/lib/terraspace/terraform/args/default.rb +16 -2
  48. data/lib/terraspace/terraform/cloud.rb +25 -0
  49. data/lib/terraspace/terraform/cloud/workspace.rb +95 -0
  50. data/lib/terraspace/terraform/runner.rb +1 -1
  51. data/lib/terraspace/util/sh.rb +1 -1
  52. data/lib/terraspace/version.rb +1 -1
  53. data/spec/fixtures/{cache_build_dir → cache_dir}/variables.tf +0 -0
  54. data/spec/fixtures/projects/hcl/aws/config/backend.tf +1 -1
  55. data/spec/fixtures/projects/hcl/google/config/backend.tf +1 -1
  56. data/spec/terraspace/seeder_spec.rb +1 -1
  57. data/spec/terraspace/terraform/hooks/builder_spec.rb +1 -1
  58. data/terraspace.gemspec +5 -4
  59. metadata +47 -13
  60. data/lib/terraspace/cli/build.rb +0 -7
@@ -0,0 +1,24 @@
1
+ class Terraspace::CLI
2
+ class Cloud < Terraspace::Command
3
+ Workspace = Terraspace::Terraform::Cloud::Workspace
4
+
5
+ desc "list", "List workspaces"
6
+ long_desc Help.text("cloud:list")
7
+ def list
8
+ Workspace.new(options).list
9
+ end
10
+
11
+ desc "destroy STACK", "Destroy workspace"
12
+ long_desc Help.text("cloud:destroy")
13
+ option :yes, aliases: :y, type: :boolean, desc: "bypass are you sure prompt"
14
+ def destroy(mod)
15
+ Workspace.new(options.merge(mod: mod)).destroy
16
+ end
17
+
18
+ desc "setup STACK", "Setup workspace"
19
+ long_desc Help.text("cloud:setup")
20
+ def setup(mod)
21
+ Workspace.new(options.merge(mod: mod)).setup
22
+ end
23
+ end
24
+ end
@@ -7,8 +7,15 @@ class Terraspace::CLI
7
7
 
8
8
  # Commander always runs Build#run
9
9
  def run
10
- Build.new(@options).run # generate and init
10
+ Terraspace::Builder.new(@options).run # generate and init
11
+ auto_create_backend
12
+ Init.new(@options.merge(calling_command: @name)).run
11
13
  Terraspace::Terraform::Runner.new(@name, @options).run
12
14
  end
15
+
16
+ def auto_create_backend
17
+ return unless @name == "apply"
18
+ Terraspace::Compiler::Backend.new(@mod).create
19
+ end
13
20
  end
14
21
  end
@@ -0,0 +1,67 @@
1
+ require 'timeout'
2
+
3
+ class Terraspace::CLI
4
+ class Init < Base
5
+ def initialize(options={})
6
+ # Original calling command. Can be from Commander which is a terraform command. IE: terraform apply
7
+ # Or can be from terraspace cloud setup. Which will be cloud-setup.
8
+ @calling_command = options[:calling_command]
9
+ super(options)
10
+ end
11
+
12
+ def run
13
+ init if init?
14
+ build_remote_dependencies # runs after terraform init, which downloads remote modules
15
+ sync_cloud
16
+ end
17
+
18
+ # Note the init will always create the Terraform Cloud Workspace
19
+ def init
20
+ # default init timeout is pretty generous in case of slow internet to download the provider plugins
21
+ init_timeout = Integer(ENV['TS_INIT_TIMEOUT'] || 600)
22
+ Timeout::timeout(init_timeout) do
23
+ Terraspace::Terraform::Runner.new("init", @options).run if !auto? && @options[:init] != false # will run on @options[:init].nil?
24
+ end
25
+ rescue Timeout::Error
26
+ logger.error "ERROR: It took too long to run terraform init. Here is the output logs of terraform init:".color(:red)
27
+ logger.error IO.read(Terraspace::Terraform::Args::Default.terraform_init_log)
28
+ end
29
+
30
+ def sync_cloud
31
+ Terraspace::Terraform::Cloud.new(@options).run if %w[apply plan destroy cloud-setup].include?(@calling_command)
32
+ end
33
+
34
+ # Currently only handles remote modules only one-level deep.
35
+ def build_remote_dependencies
36
+ modules_json_path = "#{@mod.cache_dir}/.terraform/modules/modules.json"
37
+ return unless File.exist?(modules_json_path)
38
+
39
+ initialized_modules = JSON.load(IO.read(modules_json_path))
40
+ # For example of structure see spec/fixtures/initialized/modules.json
41
+ initialized_modules["Modules"].each do |meta|
42
+ build_remote_mod(meta)
43
+ end
44
+ end
45
+
46
+ def build_remote_mod(meta)
47
+ return if local_source?(meta["Source"])
48
+ return if meta['Dir'] == '.' # root is already built
49
+
50
+ remote_mod = Mod::Remote.new(meta, @mod)
51
+ Compiler::Builder.new(remote_mod).build
52
+ end
53
+
54
+ def auto?
55
+ # command is only passed from CLI in the update specifically for this check
56
+ @options[:auto] && @calling_command == "apply"
57
+ end
58
+ private
59
+ def local_source?(s)
60
+ s =~ %r{^\.} || s =~ %r{^/}
61
+ end
62
+
63
+ def init?
64
+ %w[apply console destroy output plan providers refresh show validate cloud-setup].include?(@calling_command)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,13 @@
1
+ class Terraspace::CLI
2
+ class List
3
+ def initialize(options={})
4
+ @options = options
5
+ end
6
+
7
+ def run
8
+ Dir.glob("{app,vendor}/{modules,stacks}/*").sort.each do |path|
9
+ puts path
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,13 @@
1
1
  class Terraspace::CLI
2
2
  class New < Terraspace::Command
3
+ long_desc Help.text(:git_hook)
4
+ GitHook.cli_options.each { |args| option(*args) }
5
+ register(GitHook, "git_hook", "git_hook", "Generates new git hook")
6
+
7
+ long_desc Help.text(:shim)
8
+ Shim.cli_options.each { |args| option(*args) }
9
+ register(Shim, "shim", "shim", "Generates terraspace shim")
10
+
3
11
  long_desc Help.text(:module)
4
12
  Module.base_options.each { |args| option(*args) }
5
13
  Module.component_options.each { |args| option(*args) }
@@ -0,0 +1,33 @@
1
+ class Terraspace::CLI::New
2
+ class GitHook < Thor::Group
3
+ include Thor::Actions
4
+
5
+ def self.cli_options
6
+ [
7
+ [:envs, type: :array, default: %w[dev prod], desc: "envs to build"],
8
+ [:type, aliases: %w[t], default: "pre-push", desc: "git hook type"],
9
+ ]
10
+ end
11
+ cli_options.each { |args| class_option(*args) }
12
+
13
+ def self.source_root
14
+ File.expand_path("../../../templates/base/git_hook", __dir__)
15
+ end
16
+
17
+ def create
18
+ return unless File.exist?(".git")
19
+ dest = ".git/hooks/#{options[:type]}"
20
+ template "hook.sh", dest
21
+ chmod dest, 0755
22
+ end
23
+
24
+ private
25
+ def terraspace_build_commands
26
+ code = []
27
+ @options[:envs].each do |env|
28
+ code << %Q|TS_ENV=#{env} terraspace build placeholder|
29
+ end
30
+ code.join("\n")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,58 @@
1
+ class Terraspace::CLI::New
2
+ class Shim < Thor::Group
3
+ include Thor::Actions
4
+
5
+ def self.cli_options
6
+ [
7
+ [:path, aliases: %w[p], default: "/usr/local/bin/terraspace", desc: "path to save the shim script"],
8
+ ]
9
+ end
10
+ cli_options.each { |args| class_option(*args) }
11
+
12
+ def self.source_root
13
+ File.expand_path("../../../templates/base/shim", __dir__)
14
+ end
15
+
16
+ def set_vars
17
+ @path = @options[:path]
18
+ end
19
+
20
+ def create
21
+ return unless File.exist?(".git")
22
+ dest = @path
23
+ template "terraspace", dest
24
+ chmod dest, 0755
25
+ end
26
+
27
+ def message
28
+ dir = File.dirname(@path)
29
+ puts <<~EOL
30
+ A terraspace shim as been generated at #{@path}
31
+ Please make sure that it is found in the $PATH.
32
+
33
+ You can double check with:
34
+
35
+ which terraspace
36
+
37
+ You should see
38
+
39
+ $ which terraspace
40
+ #{@path}
41
+
42
+ If you do not, please add #{dir} to your PATH.
43
+ You can usually do so by adding this line to ~/.bash_profile and opening a new terminal to check.
44
+
45
+ export PATH=#{dir}:/$PATH
46
+
47
+ EOL
48
+ end
49
+
50
+ private
51
+ def switch_ruby_version_line
52
+ rbenv_installed = system("type rbenv 2>&1 > /dev/null")
53
+ if rbenv_installed
54
+ 'eval "$(rbenv init -)"'
55
+ end
56
+ end
57
+ end
58
+ end
@@ -11,26 +11,19 @@ class Terraspace::CLI
11
11
  end
12
12
 
13
13
  def run
14
+ Terraspace.check_project!
14
15
  build
15
16
  puts "Summary of resources based on backend storage statefiles"
16
17
  backend_expr = '.terraspace-cache/**/backend.*'
17
- backends = Dir.glob(backend_expr)
18
- backends.each do |backend|
19
- process(backend)
20
- end
18
+ # Currently summary assumes backend are within the same bucket and key prefix
19
+ backend = Dir.glob(backend_expr).find { |p| p.include?("/#{Terraspace.env}/") }
20
+ process(backend) if backend
21
21
  end
22
22
 
23
23
  # Grab the last module and build that.
24
24
  # Assume the backend key has the same prefix
25
25
  def build
26
- return if ENV['TS_SUMMARY_BUILD'] == '0'
27
-
28
- mod = @options[:mod]
29
- unless mod
30
- mod_path = Dir.glob("{app,vendor}/{modules,stacks}/*").last
31
- mod = File.basename(mod_path)
32
- end
33
- Build.new(@options.merge(mod: mod)).run # generate and init
26
+ Build::Placeholder.new(@options).build
34
27
  end
35
28
 
36
29
  def process(path)
@@ -43,6 +36,10 @@ class Terraspace::CLI
43
36
 
44
37
  info = backend.values.first # structure within the s3 or gcs key
45
38
  klass = summary_class(name)
39
+ unless klass
40
+ logger.info "Summary is unavailable for this backend: #{name}"
41
+ exit
42
+ end
46
43
  summary = klass.new(info, @options)
47
44
  summary.call
48
45
  end
@@ -1,5 +1,3 @@
1
- require "hcl_parser"
2
-
3
1
  module Terraspace::Compiler
4
2
  class Backend
5
3
  extend Memoist
@@ -9,53 +7,27 @@ module Terraspace::Compiler
9
7
  end
10
8
 
11
9
  def create
12
- klass = backend_class(backend_name)
10
+ klass = backend_interface(backend_name)
13
11
  return unless klass # in case auto-creation is not supported for specific backend
14
12
 
15
- backend = klass.new(backend_info)
16
- backend.call
13
+ interface = klass.new(backend_info)
14
+ interface.call
17
15
  end
18
16
 
19
17
  def backend_name
20
- backend_raw.keys.first # IE: s3, gcs, etc
18
+ backend.keys.first # IE: s3, gcs, etc
21
19
  end
22
20
 
23
21
  def backend_info
24
- backend_raw.values.first # structure within the s3 or gcs key
25
- end
26
-
27
- def backend_raw
28
- return {} unless exist?(backend_path)
29
- if backend_path.include?('.json')
30
- json_backend
31
- else
32
- hcl_backend
33
- end
34
- end
35
-
36
- def json_backend
37
- data = JSON.load(IO.read(backend_path))
38
- data.dig("terraform", "backend") || {}
39
- end
40
-
41
- def hcl_backend
42
- return {} unless File.exist?(backend_path)
43
- backend_raw = HclParser.load(IO.read(backend_path))
44
- return {} unless backend_raw
45
- backend_raw.dig("terraform", "backend") || {}
46
- end
47
-
48
- def exist?(path)
49
- path && File.exist?(path)
22
+ backend.values.first # structure within the s3 or gcs key
50
23
  end
51
24
 
52
- def backend_path
53
- expr = "#{@mod.cache_build_dir}/backend.tf*"
54
- Dir.glob(expr).first
25
+ def backend
26
+ Parser.new(@mod).result
55
27
  end
56
- memoize :backend_path
28
+ memoize :backend
57
29
 
58
- def backend_class(name)
30
+ def backend_interface(name)
59
31
  return unless name
60
32
  # IE: TerraspacePluginAws::Interfaces::Backend
61
33
  klass_name = Terraspace::Plugin.klass("Backend", backend: name)
@@ -0,0 +1,42 @@
1
+ require "hcl_parser"
2
+
3
+ class Terraspace::Compiler::Backend
4
+ class Parser
5
+ extend Memoist
6
+
7
+ def initialize(mod)
8
+ @mod = mod
9
+ end
10
+
11
+ def result
12
+ return {} unless exist?(backend_path)
13
+ if backend_path.include?('.json')
14
+ json_backend
15
+ else
16
+ hcl_backend
17
+ end
18
+ end
19
+
20
+ def json_backend
21
+ data = JSON.load(IO.read(backend_path))
22
+ data.dig("terraform", "backend") || {}
23
+ end
24
+
25
+ def hcl_backend
26
+ return {} unless File.exist?(backend_path)
27
+ backend_raw = HclParser.load(IO.read(backend_path))
28
+ return {} unless backend_raw
29
+ backend_raw.dig("terraform", "backend") || {}
30
+ end
31
+
32
+ def exist?(path)
33
+ path && File.exist?(path)
34
+ end
35
+
36
+ def backend_path
37
+ expr = "#{@mod.cache_dir}/backend.tf*"
38
+ Dir.glob(expr).first
39
+ end
40
+ memoize :backend_path
41
+ end
42
+ end
@@ -14,7 +14,7 @@ module Terraspace::Compiler
14
14
 
15
15
  # build common config files: provider and backend for the root module
16
16
  def build_config
17
- return unless @mod.root_module?
17
+ return unless build?
18
18
  build_config_templates
19
19
  end
20
20
 
@@ -25,11 +25,15 @@ module Terraspace::Compiler
25
25
  end
26
26
 
27
27
  def build_tfvars
28
- return unless @mod.root_module?
28
+ return unless build?
29
29
  Strategy::Tfvar.new(@mod).run # writer within Strategy to control file ordering
30
30
  end
31
31
 
32
32
  private
33
+ def build?
34
+ @mod.type == "stack" || @mod.root_module?
35
+ end
36
+
33
37
  def build_config_templates
34
38
  expr = "#{Terraspace.root}/config/terraform/**/*"
35
39
  Dir.glob(expr).each do |path|
@@ -19,6 +19,7 @@ module Terraspace::Compiler
19
19
  # only remove .tf* files. leaving cache .terraform and terraform.state files
20
20
  def remove_materialized_artifacts
21
21
  Dir.glob("#{Terraspace.cache_root}/**/*").each do |path|
22
+ next unless within_env?(path)
22
23
  next if path.include?(".tfstate")
23
24
  FileUtils.rm_f(path) if File.file?(path)
24
25
  end
@@ -31,7 +32,7 @@ module Terraspace::Compiler
31
32
  # the file again. With verbose logging, it shows it twice so that's a little bit confusing though.
32
33
  #
33
34
  # def remove_materialized_artifacts_dot_terraform
34
- # expr = "#{@mod.cache_build_dir}/.terraform/**/*"
35
+ # expr = "#{@mod.cache_dir}/.terraform/**/*"
35
36
  #
36
37
  # Dir.glob(expr).each do |path|
37
38
  # logger.info "path #{path}"
@@ -40,7 +41,23 @@ module Terraspace::Compiler
40
41
 
41
42
  def remove_empty_directories
42
43
  return unless File.exist?(Terraspace.cache_root)
43
- Dir["#{Terraspace.cache_root}/**/"].reverse_each { |d| Dir.rmdir d if Dir.entries(d).size == 2 }
44
+ Dir["#{Terraspace.cache_root}/**/"].reverse_each do |d|
45
+ next unless within_env?(d)
46
+ Dir.rmdir(d) if Dir.entries(d).size == 2
47
+ end
48
+ end
49
+
50
+ # Only remove files within an env for the TFC VCS-Workflow.
51
+ # We dont want to run:
52
+ #
53
+ # TS_ENV=prod terraspace up demo
54
+ #
55
+ # And that to delete the .terraspace-cache/us-west-2/dev files
56
+ #
57
+ # May need to allow further customization to this if user project has a stack named the same as the env.
58
+ #
59
+ def within_env?(path)
60
+ path.include?("/#{Terraspace.env}/")
44
61
  end
45
62
  end
46
63
  end