terraspace 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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -3
  3. data/README.md +34 -13
  4. data/lib/templates/base/git_hook/hook.sh +1 -1
  5. data/lib/templates/base/project/.gitignore +1 -0
  6. data/lib/templates/base/project/README.md +17 -0
  7. data/lib/terraspace.rb +4 -0
  8. data/lib/terraspace/all/base.rb +8 -0
  9. data/lib/terraspace/all/grapher.rb +129 -0
  10. data/lib/terraspace/all/preview.rb +43 -0
  11. data/lib/terraspace/all/runner.rb +169 -0
  12. data/lib/terraspace/all/summary.rb +99 -0
  13. data/lib/terraspace/app.rb +31 -8
  14. data/lib/terraspace/booter.rb +9 -0
  15. data/lib/terraspace/builder.rb +53 -20
  16. data/lib/terraspace/cli.rb +39 -12
  17. data/lib/terraspace/cli/all.rb +63 -0
  18. data/lib/terraspace/cli/build/placeholder.rb +2 -5
  19. data/lib/terraspace/cli/bundle.rb +1 -1
  20. data/lib/terraspace/cli/check_setup.rb +17 -5
  21. data/lib/terraspace/cli/cloud.rb +19 -3
  22. data/lib/terraspace/cli/cloud/runs.rb +24 -0
  23. data/lib/terraspace/cli/commander.rb +6 -3
  24. data/lib/terraspace/cli/down.rb +20 -0
  25. data/lib/terraspace/cli/help/cloud/runs/list.md +36 -0
  26. data/lib/terraspace/cli/help/cloud/runs/prune.md +25 -0
  27. data/lib/terraspace/cli/help/cloud/sync.md +19 -0
  28. data/lib/terraspace/cli/help/log.md +42 -0
  29. data/lib/terraspace/cli/init.rb +35 -7
  30. data/lib/terraspace/cli/list.rb +14 -1
  31. data/lib/terraspace/cli/log.rb +112 -0
  32. data/lib/terraspace/cli/log/concern.rb +24 -0
  33. data/lib/terraspace/cli/logs.rb +15 -0
  34. data/lib/terraspace/cli/logs/tasks.rb +32 -0
  35. data/lib/terraspace/cli/new/git_hook.rb +1 -1
  36. data/lib/terraspace/cli/summary.rb +1 -1
  37. data/lib/terraspace/cli/tfc_concern.rb +14 -0
  38. data/lib/terraspace/cli/up.rb +32 -0
  39. data/lib/terraspace/compiler/builder.rb +3 -3
  40. data/lib/terraspace/compiler/cleaner.rb +1 -1
  41. data/lib/terraspace/compiler/cleaner/backend_change.rb +21 -7
  42. data/lib/terraspace/compiler/dirs_concern.rb +47 -0
  43. data/lib/terraspace/compiler/dsl/syntax/helpers/common.rb +26 -1
  44. data/lib/terraspace/core.rb +11 -2
  45. data/lib/terraspace/dependency/graph.rb +139 -0
  46. data/lib/terraspace/dependency/node.rb +38 -0
  47. data/lib/terraspace/dependency/registry.rb +11 -0
  48. data/lib/terraspace/logger.rb +6 -18
  49. data/lib/terraspace/logger/formatter.rb +13 -0
  50. data/lib/terraspace/mod.rb +7 -1
  51. data/lib/terraspace/plugin/summary/interface.rb +1 -0
  52. data/lib/terraspace/seeder/where.rb +6 -2
  53. data/lib/terraspace/shell.rb +79 -0
  54. data/lib/terraspace/terraform/api.rb +7 -40
  55. data/lib/terraspace/terraform/api/base.rb +7 -0
  56. data/lib/terraspace/terraform/api/client.rb +23 -3
  57. data/lib/terraspace/terraform/api/http.rb +14 -34
  58. data/lib/terraspace/terraform/api/http/concern.rb +10 -0
  59. data/lib/terraspace/terraform/api/runs.rb +28 -0
  60. data/lib/terraspace/terraform/api/token.rb +65 -0
  61. data/lib/terraspace/terraform/api/var.rb +20 -6
  62. data/lib/terraspace/terraform/api/vars.rb +2 -1
  63. data/lib/terraspace/terraform/api/workspace.rb +98 -0
  64. data/lib/terraspace/terraform/args/default.rb +48 -21
  65. data/lib/terraspace/terraform/cloud/runs.rb +13 -0
  66. data/lib/terraspace/terraform/cloud/runs/base.rb +33 -0
  67. data/lib/terraspace/terraform/cloud/runs/item_presenter.rb +37 -0
  68. data/lib/terraspace/terraform/cloud/runs/lister.rb +22 -0
  69. data/lib/terraspace/terraform/cloud/runs/pruner.rb +109 -0
  70. data/lib/terraspace/terraform/cloud/sync.rb +41 -0
  71. data/lib/terraspace/terraform/cloud/syncer.rb +52 -0
  72. data/lib/terraspace/terraform/cloud/workspace.rb +10 -21
  73. data/lib/terraspace/terraform/hooks/builder.rb +1 -1
  74. data/lib/terraspace/terraform/remote_state/fetcher.rb +122 -0
  75. data/lib/terraspace/terraform/remote_state/marker/output.rb +39 -0
  76. data/lib/terraspace/terraform/remote_state/marker/pretty_tracer.rb +37 -0
  77. data/lib/terraspace/terraform/remote_state/output_proxy.rb +29 -0
  78. data/lib/terraspace/terraform/runner.rb +24 -14
  79. data/lib/terraspace/util.rb +1 -5
  80. data/lib/terraspace/util/pretty.rb +18 -0
  81. data/lib/terraspace/version.rb +1 -1
  82. data/spec/fixtures/fetcher/c1.json +37 -0
  83. data/spec/fixtures/parser/cache_dirs/all/01-test.auto.tfvars +5 -0
  84. data/spec/fixtures/parser/cache_dirs/depends_on/01-test.auto.tfvars +2 -0
  85. data/spec/fixtures/parser/cache_dirs/output/01-test.auto.tfvars +2 -0
  86. data/spec/fixtures/summary/down.log +12 -0
  87. data/spec/fixtures/summary/output.log +5 -0
  88. data/spec/fixtures/summary/plan/error.log +20 -0
  89. data/spec/fixtures/summary/plan/success.log +17 -0
  90. data/spec/fixtures/summary/show.log +22 -0
  91. data/spec/fixtures/summary/up/error.log +13 -0
  92. data/spec/fixtures/summary/up/success.log +63 -0
  93. data/spec/fixtures/summary/validate/error.log +13 -0
  94. data/spec/fixtures/summary/validate/success.log +5 -0
  95. data/spec/terraspace/all/grapher_spec.rb +38 -0
  96. data/spec/terraspace/all/runner_spec.rb +48 -0
  97. data/spec/terraspace/all/summary_spec.rb +93 -0
  98. data/spec/terraspace/dependency/graph_spec.rb +162 -0
  99. data/spec/terraspace/seeder_spec.rb +0 -1
  100. data/spec/terraspace/terraform/remote_state/fetcher_spec.rb +52 -0
  101. data/terraspace.gemspec +5 -1
  102. metadata +137 -5
  103. data/lib/terraspace/terraform/cloud.rb +0 -25
  104. data/lib/terraspace/util/sh.rb +0 -19
@@ -0,0 +1,24 @@
1
+ class Terraspace::CLI::Log
2
+ module Concern
3
+ # Filters for lines that belong to the last ran process pid
4
+ def readlines(path)
5
+ lines = IO.readlines(path)
6
+ found = lines.reverse.find do |line|
7
+ pid(line) # search in reverse order for lines with interesting info
8
+ end
9
+ unless found
10
+ puts "WARN: Could not find the pid in the logfile #{Terraspace::Util.pretty_path(path)}".color(:yellow)
11
+ return []
12
+ end
13
+
14
+ pid = pid(found)
15
+ lines.select {|l| l.include?(" ##{pid} ") }
16
+ end
17
+
18
+ # [2020-09-06T21:58:25 #11313 terraspace up b1]:
19
+ def pid(line)
20
+ md = line.match(/:\d{2} #(\d+) /)
21
+ md[1] if md
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ class Terraspace::CLI
2
+ class Logs < Terraspace::Command
3
+ desc "truncate", "Truncates logs. IE: Removes contents and zero bytes the files"
4
+ long_desc Help.text("logs/truncate")
5
+ def truncate
6
+ Tasks.new(options).truncate
7
+ end
8
+
9
+ desc "remove", "Removes logs"
10
+ long_desc Help.text("logs/remove")
11
+ def remove
12
+ Tasks.new(options).remove
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ class Terraspace::CLI::Logs
2
+ class Tasks
3
+ def initialize(options={})
4
+ @options = options
5
+ end
6
+
7
+ def truncate
8
+ puts "Truncating log files in #{pretty_log_root}/" unless @options[:mute]
9
+ log_files.each do |path|
10
+ File.open(path, "w").close # truncates files
11
+ end
12
+ end
13
+
14
+ def remove
15
+ puts "Removing all files in #{pretty_log_root}/" unless @options[:mute]
16
+ FileUtils.rm_rf(log_root)
17
+ FileUtils.mkdir_p(log_root)
18
+ end
19
+
20
+ def log_files
21
+ Dir.glob("#{log_root}/**/*.log")
22
+ end
23
+
24
+ def pretty_log_root
25
+ Terraspace::Util.pretty_path(log_root)
26
+ end
27
+
28
+ def log_root
29
+ Terraspace.config.log.root
30
+ end
31
+ end
32
+ end
@@ -25,7 +25,7 @@ class Terraspace::CLI::New
25
25
  def terraspace_build_commands
26
26
  code = []
27
27
  @options[:envs].each do |env|
28
- code << %Q|TS_ENV=#{env} terraspace build placeholder|
28
+ code << %Q|TS_ENV=#{env} terraspace build|
29
29
  end
30
30
  code.join("\n")
31
31
  end
@@ -16,7 +16,7 @@ class Terraspace::CLI
16
16
  puts "Summary of resources based on backend storage statefiles"
17
17
  backend_expr = '.terraspace-cache/**/backend.*'
18
18
  # Currently summary assumes backend are within the same bucket and key prefix
19
- backend = Dir.glob(backend_expr).first
19
+ backend = Dir.glob(backend_expr).find { |p| p.include?("/#{Terraspace.env}/") }
20
20
  process(backend) if backend
21
21
  end
22
22
 
@@ -0,0 +1,14 @@
1
+ class Terraspace::CLI
2
+ module TfcConcern
3
+ extend Memoist
4
+
5
+ def tfc?
6
+ !!backend["remote"]
7
+ end
8
+
9
+ def backend
10
+ Terraspace::Compiler::Backend::Parser.new(@mod).result
11
+ end
12
+ memoize :backend
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ class Terraspace::CLI
2
+ class Up < Base
3
+ include TfcConcern
4
+
5
+ def run
6
+ build
7
+ if @options[:yes] && !tfc?
8
+ plan
9
+ Commander.new("apply", @options.merge(plan: plan_path)).run
10
+ else
11
+ Commander.new("apply", @options).run
12
+ end
13
+ end
14
+
15
+ private
16
+ # must build to compute tfc?
17
+ def build
18
+ Terraspace::Builder.new(@options).run
19
+ @options[:build] = false
20
+ end
21
+
22
+ def plan
23
+ FileUtils.mkdir_p(File.dirname(plan_path))
24
+ Commander.new("plan", @options.merge(out: plan_path)).run
25
+ end
26
+
27
+ def plan_path
28
+ @@timestamp ||= Time.now.utc.strftime("%Y%m%d%H%M%S")
29
+ "#{Terraspace.tmp_root}/plans/#{@mod.name}-#{@@timestamp}.plan"
30
+ end
31
+ end
32
+ end
@@ -8,14 +8,14 @@ module Terraspace::Compiler
8
8
 
9
9
  def build
10
10
  build_config
11
- build_module
11
+ build_module if @mod.resolved
12
12
  build_tfvars
13
13
  end
14
14
 
15
15
  # build common config files: provider and backend for the root module
16
16
  def build_config
17
17
  return unless build?
18
- build_config_templates
18
+ build_config_terraform
19
19
  end
20
20
 
21
21
  def build_module
@@ -34,7 +34,7 @@ module Terraspace::Compiler
34
34
  @mod.type == "stack" || @mod.root_module?
35
35
  end
36
36
 
37
- def build_config_templates
37
+ def build_config_terraform
38
38
  expr = "#{Terraspace.root}/config/terraform/**/*"
39
39
  Dir.glob(expr).each do |path|
40
40
  next unless File.file?(path)
@@ -5,7 +5,7 @@ module Terraspace::Compiler
5
5
  end
6
6
 
7
7
  def clean
8
- return if ENV['TS_NO_CLEAN']
8
+ return if ENV['TS_CLEAN'] == '0'
9
9
  backend_change_purge
10
10
  remove_materialized_artifacts
11
11
  # remove_materialized_artifacts_dot_terraform
@@ -9,14 +9,9 @@ class Terraspace::Compiler::Cleaner
9
9
  def purge
10
10
  return unless purge?
11
11
 
12
+ are_you_sure? if local_statefile_exist?
12
13
  cache_root = Terraspace::Util.pretty_path(Terraspace.cache_root)
13
- message =<<~EOL
14
- Backend change detected. Will remove #{cache_root} for complete reinitialization
15
- WARN: If you are using local storage for state, this will remove it.
16
- Will remove #{cache_root}
17
- EOL
18
- sure?(message.strip)
19
- logger.info "Backend change detected. Removing #{cache_root} for complete reinitialization"
14
+ logger.debug "Backend change detected. Removing #{cache_root} for complete reinitialization"
20
15
  FileUtils.rm_rf(Terraspace.cache_root)
21
16
  end
22
17
 
@@ -26,6 +21,15 @@ class Terraspace::Compiler::Cleaner
26
21
  current_backend != fresh_backend
27
22
  end
28
23
 
24
+ def local_statefile_exist?
25
+ # Note: Will not go into .terraform folders. No need to for terraform.tfstate
26
+ Dir.glob("#{Terraspace.cache_root}/**/*").each do |path|
27
+ basename = File.basename(path)
28
+ return true if basename == 'terraform.tfstate'
29
+ end
30
+ false
31
+ end
32
+
29
33
  def current_backend
30
34
  materialized_path = find_src_path("#{@mod.cache_dir}/backend*")
31
35
  IO.read(materialized_path) if materialized_path
@@ -37,6 +41,16 @@ class Terraspace::Compiler::Cleaner
37
41
  end
38
42
 
39
43
  private
44
+ def are_you_sure?
45
+ cache_root = Terraspace::Util.pretty_path(Terraspace.cache_root)
46
+ message =<<~EOL
47
+ Backend change detected. Will remove #{cache_root} for complete reinitialization
48
+ #{"WARN: You are using local storage for state, this will remove it.".color(:yellow)}
49
+ Will remove #{cache_root} and all terraform.tfstate files
50
+ EOL
51
+ sure?(message.strip) # from Util
52
+ end
53
+
40
54
  def find_src_path(expr)
41
55
  path = Dir.glob(expr).first
42
56
  path if path && File.exist?(path)
@@ -0,0 +1,47 @@
1
+ module Terraspace::Compiler
2
+ module DirsConcern
3
+ extend ActiveSupport::Concern
4
+ extend Memoist
5
+
6
+ def cache_dirs
7
+ cache_dirs = []
8
+ with_each_mod("stacks") do |mod|
9
+ cache_dirs << mod.cache_dir
10
+ end
11
+ cache_dirs
12
+ end
13
+
14
+ def with_each_mod(type_dir)
15
+ mod_names(type_dir).each do |mod_name|
16
+ consider_stacks = type_dir == "stacks"
17
+ mod = Terraspace::Mod.new(mod_name, @options.merge(consider_stacks: consider_stacks))
18
+ yield(mod)
19
+ end
20
+ end
21
+
22
+ def mod_names(type_dir)
23
+ names, built = [], []
24
+ local_paths(type_dir).each do |path|
25
+ next unless File.directory?(path)
26
+ mod_name = File.basename(path)
27
+ next if built.include?(mod_name) # ensures modules in app folder take higher precedence than vendor folder
28
+ names << mod_name
29
+ end
30
+ names
31
+ end
32
+ memoize :mod_names
33
+
34
+ def local_paths(type_dir)
35
+ dirs("app/#{type_dir}/*") + dirs("vendor/#{type_dir}/*")
36
+ end
37
+
38
+ def dirs(path)
39
+ Dir.glob("#{Terraspace.root}/#{path}")
40
+ end
41
+
42
+ def stack_names
43
+ mod_names("stacks") - Terraspace.config.all.ignore_stacks
44
+ end
45
+ memoize :stack_names
46
+ end
47
+ end
@@ -1,7 +1,8 @@
1
1
  module Terraspace::Compiler::Dsl::Syntax::Helpers
2
2
  module Common
3
3
  extend Memoist
4
-
4
+ Fetcher = Terraspace::Terraform::RemoteState::Fetcher
5
+ Marker = Terraspace::Terraform::RemoteState::Marker
5
6
  Meta = Terraspace::Compiler::Dsl::Meta
6
7
 
7
8
  def var
@@ -24,5 +25,29 @@ module Terraspace::Compiler::Dsl::Syntax::Helpers
24
25
  command = ["terraspace"] + args
25
26
  command.join(separator)
26
27
  end
28
+
29
+ def terraform_output(identifier, options={})
30
+ if @mod.resolved # dependencies have been resolved
31
+ Fetcher.new(@mod, identifier, options).output
32
+ else
33
+ Marker::Output.new(@mod, identifier, options).build
34
+ end
35
+ end
36
+
37
+ def depends_on(*child_names, **options)
38
+ child_names.flatten!
39
+ child_names.map do |child_name|
40
+ each_depends_on(child_name, options)
41
+ end.join("\n")
42
+ end
43
+
44
+ def each_depends_on(child_name, options={})
45
+ if @mod.resolved # dependencies have been resolved
46
+ # Note: A generated line is not really needed. Dependencies are stored in memory. Added to assist users with debugging
47
+ "# #{@mod.name} depends on #{child_name}"
48
+ else
49
+ Marker::Output.new(@mod, child_name, options).build
50
+ end
51
+ end
27
52
  end
28
53
  end
@@ -27,6 +27,10 @@ module Terraspace
27
27
  end
28
28
  memoize :tmp_root
29
29
 
30
+ def log_root
31
+ "#{root}/log"
32
+ end
33
+
30
34
  def configure(&block)
31
35
  App.instance.configure(&block)
32
36
  end
@@ -38,10 +42,15 @@ module Terraspace
38
42
  end
39
43
  memoize :config
40
44
 
45
+ @@logger = nil
41
46
  def logger
42
- config.logger
47
+ @@logger ||= config.logger
48
+ end
49
+
50
+ # allow different logger when running up all
51
+ def logger=(v)
52
+ @@logger = v
43
53
  end
44
- memoize :logger
45
54
 
46
55
  def check_project!
47
56
  return if File.exist?("#{Terraspace.root}/config/app.rb")
@@ -0,0 +1,139 @@
1
+ module Terraspace::Dependency
2
+ class Graph
3
+ include Terraspace::Util::Logging
4
+
5
+ attr_reader :nodes
6
+ def initialize(stack_names, dependencies, options={})
7
+ @stack_names, @dependencies, @options = stack_names, dependencies, options
8
+ @nodes = []
9
+ @batches = []
10
+ end
11
+
12
+ def build
13
+ precreate_all_nodes
14
+ build_nodes_with_dependencies # @nodes has dependency graph info afterwards
15
+ check_circular_dependencies!
16
+ @nodes = filter_nodes
17
+ check_empty_nodes!
18
+ build_batches
19
+ clean_batches
20
+ @batches
21
+ end
22
+
23
+ def check_empty_nodes!
24
+ return unless @nodes.empty?
25
+ logger.error "ERROR: No stacks were found that match: #{@options[:stacks].join(' ')}".color(:red)
26
+ exit 1
27
+ end
28
+
29
+ def check_circular_dependencies!
30
+ @nodes.each do |node|
31
+ check_cycle(node)
32
+ end
33
+ end
34
+
35
+ MAX_CYCLE_DEPTH = Integer(ENV['TS_MAX_CYCLE_DEPTH'] || 100)
36
+ def check_cycle(node, depth=0, list=[])
37
+ if depth > MAX_CYCLE_DEPTH
38
+ logger.error "ERROR: It seems like there is a circular dependency! Stacks involved: #{list.uniq}".color(:red)
39
+ exit 1
40
+ end
41
+ node.parents.each do |parent|
42
+ check_cycle(parent, depth+1, list += [parent])
43
+ end
44
+ end
45
+
46
+ def precreate_all_nodes
47
+ @stack_names.each do |name|
48
+ node = Node.find_or_create_by(name: name)
49
+ save_node(node)
50
+ end
51
+ end
52
+
53
+ def build_nodes_with_dependencies
54
+ @dependencies.each do |item|
55
+ parent_name, child_name = item.split(':')
56
+ save_node_parent(parent_name, child_name)
57
+ end
58
+ end
59
+
60
+ def save_node_parent(parent_name, child_name)
61
+ parent = Node.find_by(name: parent_name)
62
+ child = Node.find_by(name: child_name)
63
+ child.parent!(parent)
64
+ save_node(parent)
65
+ save_node(child)
66
+ end
67
+
68
+ def build_batches
69
+ @batches[0] = Set.new(leaves)
70
+ leaves.each do |leaf|
71
+ leaf.parents.each do |parent|
72
+ build_batch(parent)
73
+ end
74
+ end
75
+ end
76
+
77
+ # So stack nodes dont get deployed more than once and too early
78
+ def clean_batches
79
+ all = Set.new
80
+ # batch is a set
81
+ @batches.reverse.each do |batch|
82
+ batch.each do |node|
83
+ batch.delete(node) if all.include?(node)
84
+ end
85
+ all += batch
86
+ end
87
+ @batches.reject! { |batch| batch.empty? }
88
+ @batches
89
+ end
90
+
91
+ def build_batch(leaf, depth=1)
92
+ @batches[depth] ||= Set.new
93
+ @batches[depth] << leaf
94
+ leaf.parents.each do |parent|
95
+ build_batch(parent, depth+1)
96
+ end
97
+ end
98
+
99
+ def filter_nodes
100
+ @filtered = []
101
+ top_nodes.each { |node| apply_filter(node) }
102
+ # draw_full_graph option is only used internally by All::Grapher
103
+ update_parents! unless @options[:draw_full_graph]
104
+ @options[:draw_full_graph] ? @nodes : @filtered
105
+ end
106
+
107
+ # remove missing parents references since they will be filtered out
108
+ def update_parents!
109
+ @filtered.each do |node|
110
+ new_parents = node.parents & @filtered
111
+ node.parents = new_parents
112
+ end
113
+ end
114
+
115
+ def apply_filter(parent, keep=false)
116
+ keep ||= @options[:stacks].blank?
117
+ keep ||= @options[:stacks].include?(parent.name) # apply filter
118
+ if keep
119
+ parent.filtered = true
120
+ @filtered << parent
121
+ end
122
+ parent.children.sort_by(&:name).each do |child|
123
+ apply_filter(child, keep)
124
+ end
125
+ end
126
+
127
+ def leaves
128
+ @nodes.select { |n| n.children.empty? }.sort_by(&:name)
129
+ end
130
+
131
+ def top_nodes
132
+ @nodes.select { |n| n.parents.empty? }.sort_by(&:name)
133
+ end
134
+
135
+ def save_node(node)
136
+ @nodes << node unless @nodes.detect { |n| n.name == node.name }
137
+ end
138
+ end
139
+ end