terraspace 0.5.10 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +2 -2
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +4 -3
  4. data/.github/ISSUE_TEMPLATE/question.md +2 -2
  5. data/CHANGELOG.md +30 -7
  6. data/README.md +2 -0
  7. data/lib/terraspace/app.rb +36 -15
  8. data/lib/terraspace/app/inits.rb +13 -0
  9. data/lib/terraspace/booter.rb +19 -2
  10. data/lib/terraspace/cli.rb +13 -6
  11. data/lib/terraspace/cli/build/placeholder.rb +6 -1
  12. data/lib/terraspace/cli/commander.rb +1 -1
  13. data/lib/terraspace/cli/down.rb +1 -1
  14. data/lib/terraspace/cli/help/{cloud → tfc}/destroy.md +1 -1
  15. data/lib/terraspace/cli/help/{cloud → tfc}/list.md +1 -1
  16. data/lib/terraspace/cli/help/{cloud → tfc}/runs/list.md +3 -3
  17. data/lib/terraspace/cli/help/{cloud → tfc}/runs/prune.md +3 -3
  18. data/lib/terraspace/cli/help/{cloud → tfc}/sync.md +3 -3
  19. data/lib/terraspace/cli/init.rb +21 -8
  20. data/lib/terraspace/cli/state.rb +10 -0
  21. data/lib/terraspace/cli/{cloud.rb → tfc.rb} +3 -3
  22. data/lib/terraspace/cli/{cloud → tfc}/runs.rb +4 -4
  23. data/lib/terraspace/cli/up.rb +5 -3
  24. data/lib/terraspace/compiler/builder.rb +2 -0
  25. data/lib/terraspace/compiler/dirs_concern.rb +11 -1
  26. data/lib/terraspace/compiler/select.rb +28 -0
  27. data/lib/terraspace/compiler/strategy/tfvar.rb +16 -4
  28. data/lib/terraspace/compiler/strategy/tfvar/layer.rb +75 -52
  29. data/lib/terraspace/layering.rb +24 -0
  30. data/lib/terraspace/logger.rb +8 -1
  31. data/lib/terraspace/mod.rb +18 -3
  32. data/lib/terraspace/plugin/expander/friendly.rb +10 -0
  33. data/lib/terraspace/plugin/expander/interface.rb +6 -1
  34. data/lib/terraspace/plugin/summary/interface.rb +1 -1
  35. data/lib/terraspace/shell.rb +16 -1
  36. data/lib/terraspace/shell/error.rb +1 -1
  37. data/lib/terraspace/terraform/api/runs.rb +13 -2
  38. data/lib/terraspace/terraform/api/token.rb +2 -2
  39. data/lib/terraspace/terraform/api/var.rb +1 -1
  40. data/lib/terraspace/terraform/api/vars.rb +1 -1
  41. data/lib/terraspace/terraform/api/vars/base.rb +2 -0
  42. data/lib/terraspace/terraform/api/vars/json.rb +13 -1
  43. data/lib/terraspace/terraform/api/workspace.rb +10 -3
  44. data/lib/terraspace/terraform/args/default.rb +23 -13
  45. data/lib/terraspace/terraform/ihooks/after/plan.rb +17 -0
  46. data/lib/terraspace/terraform/ihooks/base.rb +8 -0
  47. data/lib/terraspace/terraform/ihooks/before/plan.rb +14 -0
  48. data/lib/terraspace/terraform/remote_state/fetcher.rb +1 -1
  49. data/lib/terraspace/terraform/runner.rb +12 -0
  50. data/lib/terraspace/terraform/{cloud → tfc}/runs.rb +1 -1
  51. data/lib/terraspace/terraform/{cloud → tfc}/runs/base.rb +1 -1
  52. data/lib/terraspace/terraform/{cloud → tfc}/runs/item_presenter.rb +1 -1
  53. data/lib/terraspace/terraform/{cloud → tfc}/runs/lister.rb +1 -1
  54. data/lib/terraspace/terraform/{cloud → tfc}/runs/pruner.rb +1 -1
  55. data/lib/terraspace/terraform/{cloud → tfc}/sync.rb +2 -2
  56. data/lib/terraspace/terraform/{cloud → tfc}/syncer.rb +1 -1
  57. data/lib/terraspace/terraform/{cloud → tfc}/workspace.rb +2 -3
  58. data/lib/terraspace/util/pretty.rb +2 -1
  59. data/lib/terraspace/version.rb +1 -1
  60. metadata +26 -19
  61. data/lib/terraspace/app/hooks.rb +0 -18
@@ -3,10 +3,9 @@ require 'timeout'
3
3
  class Terraspace::CLI
4
4
  class Init < Base
5
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)
6
+ # terraform init output goes to default Terraspace.logger.info which is stderr
7
+ # Unless the logger has been overridden.
8
+ super(options.merge(log_to_stderr: true))
10
9
  end
11
10
 
12
11
  def run
@@ -29,7 +28,11 @@ class Terraspace::CLI
29
28
  end
30
29
 
31
30
  def sync_cloud
32
- Terraspace::Terraform::Cloud::Sync.new(@options).run if %w[apply plan destroy cloud-setup].include?(@calling_command)
31
+ Terraspace::Terraform::Tfc::Sync.new(@options).run if sync_cloud?
32
+ end
33
+
34
+ def sync_cloud?
35
+ %w[apply down plan up].include?(calling_command)
33
36
  end
34
37
 
35
38
  # Currently only handles remote modules only one-level deep.
@@ -54,16 +57,21 @@ class Terraspace::CLI
54
57
 
55
58
  def auto?
56
59
  # command is only passed from CLI in the update specifically for this check
57
- @options[:auto] && @calling_command == "apply"
60
+ @options[:auto] && calling_command == "up"
58
61
  end
59
62
  private
60
63
  def local_source?(s)
61
64
  s =~ %r{^\.} || s =~ %r{^/}
62
65
  end
63
66
 
67
+ def auto_init?
68
+ # terraspace commands not terraform commands. included some extra terraform commands here in case terrapace adds those later
69
+ commands = %w[all apply console down output plan providers refresh show state up validate]
70
+ commands.include?(calling_command)
71
+ end
72
+
64
73
  def run_init?
65
- commands = %w[apply console destroy output plan providers refresh show validate cloud-setup]
66
- return false unless commands.include?(@calling_command)
74
+ return false unless auto_init?
67
75
  mode = ENV['TS_INIT_MODE'] || Terraspace.config.init.mode
68
76
  case mode.to_sym
69
77
  when :auto
@@ -87,5 +95,10 @@ class Terraspace::CLI
87
95
  end
88
96
  !!provider
89
97
  end
98
+
99
+ # only top level command considered
100
+ def calling_command
101
+ ARGV[0]
102
+ end
90
103
  end
91
104
  end
@@ -0,0 +1,10 @@
1
+ class Terraspace::CLI
2
+ class State < Base
3
+ def run
4
+ @name = "state #{@options[:subcommand]}" # command name. IE: state list
5
+ Terraspace::Builder.new(@options).run
6
+ Init.new(@options).run
7
+ Terraspace::Terraform::Runner.new(@name, @options).run
8
+ end
9
+ end
10
+ end
@@ -1,7 +1,7 @@
1
1
  class Terraspace::CLI
2
- class Cloud < Terraspace::Command
3
- Syncer = Terraspace::Terraform::Cloud::Syncer
4
- Workspace = Terraspace::Terraform::Cloud::Workspace
2
+ class Tfc < Terraspace::Command
3
+ Syncer = Terraspace::Terraform::Tfc::Syncer
4
+ Workspace = Terraspace::Terraform::Tfc::Workspace
5
5
 
6
6
  yes_option = Proc.new {
7
7
  option :yes, aliases: :y, type: :boolean, desc: "bypass are you sure prompt"
@@ -1,10 +1,10 @@
1
- class Terraspace::CLI::Cloud
1
+ class Terraspace::CLI::Tfc
2
2
  class Runs < Terraspace::Command
3
3
  Help = Terraspace::CLI::Help
4
- Runs = Terraspace::Terraform::Cloud::Runs
4
+ Runs = Terraspace::Terraform::Tfc::Runs
5
5
 
6
6
  desc "list STACK", "List runs."
7
- long_desc Help.text("cloud:runs:list")
7
+ long_desc Help.text("tfc:runs:list")
8
8
  option :format, desc: "Output formats: #{CliFormat.formats.join(', ')}"
9
9
  option :status, default: %w[pending planned], type: :array, desc: "Filter by statuses: pending, planned, all"
10
10
  def list(mod)
@@ -12,7 +12,7 @@ class Terraspace::CLI::Cloud
12
12
  end
13
13
 
14
14
  desc "prune STACK", "Prune runs that are possible to cancel or discard."
15
- long_desc Help.text("cloud:runs:prune")
15
+ long_desc Help.text("tfc:runs:prune")
16
16
  option :noop, type: :boolean, desc: "Shows what would be cancelled/discarded."
17
17
  option :yes, aliases: :y, type: :boolean, desc: "bypass are you sure prompt"
18
18
  def prune(mod)
@@ -1,10 +1,12 @@
1
+ require 'securerandom'
2
+
1
3
  class Terraspace::CLI
2
4
  class Up < Base
3
5
  include TfcConcern
4
6
 
5
7
  def run
6
8
  build
7
- if @options[:yes] && !tfc?
9
+ if @options[:yes] && !@options[:plan] && !tfc?
8
10
  plan
9
11
  Commander.new("apply", @options.merge(plan: plan_path)).run
10
12
  else
@@ -25,8 +27,8 @@ class Terraspace::CLI
25
27
  end
26
28
 
27
29
  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
+ @@random ||= SecureRandom.hex
31
+ "#{Terraspace.tmp_root}/plans/#{@mod.name}-#{@@random}.plan"
30
32
  end
31
33
  end
32
34
  end
@@ -39,6 +39,7 @@ module Terraspace::Compiler
39
39
  expr = "#{Terraspace.root}/config/terraform/**/*"
40
40
  Dir.glob(expr).each do |path|
41
41
  next unless File.file?(path)
42
+ next if path.include?('config/terraform/tfvars')
42
43
  build_config_file(basename(path))
43
44
  end
44
45
  end
@@ -73,6 +74,7 @@ module Terraspace::Compiler
73
74
  def skip?(src_path)
74
75
  return true unless File.file?(src_path)
75
76
  # certain folders will be skipped
77
+ src_path.include?("#{@mod.root}/tfvars") ||
76
78
  src_path.include?("#{@mod.root}/config/args") ||
77
79
  src_path.include?("#{@mod.root}/config/hooks") ||
78
80
  src_path.include?("#{@mod.root}/test")
@@ -23,6 +23,7 @@ module Terraspace::Compiler
23
23
  names, built = [], []
24
24
  local_paths(type_dir).each do |path|
25
25
  next unless File.directory?(path)
26
+ next unless select_stack?(type_dir, path)
26
27
  mod_name = File.basename(path)
27
28
  next if built.include?(mod_name) # ensures modules in app folder take higher precedence than vendor folder
28
29
  names << mod_name
@@ -31,6 +32,15 @@ module Terraspace::Compiler
31
32
  end
32
33
  memoize :mod_names
33
34
 
35
+ # Examples:
36
+ # type_dir stacks
37
+ # path /home/ec2-user/environment/downloads/infra/app/stacks/demo
38
+ def select_stack?(type_dir, path)
39
+ return true unless type_dir == "stacks"
40
+ select = Terraspace::Compiler::Select.new(path)
41
+ select.selected?
42
+ end
43
+
34
44
  def local_paths(type_dir)
35
45
  dirs("app/#{type_dir}/*") + dirs("vendor/#{type_dir}/*")
36
46
  end
@@ -40,7 +50,7 @@ module Terraspace::Compiler
40
50
  end
41
51
 
42
52
  def stack_names
43
- mod_names("stacks") - Terraspace.config.all.ignore_stacks
53
+ mod_names("stacks")
44
54
  end
45
55
  memoize :stack_names
46
56
  end
@@ -0,0 +1,28 @@
1
+ module Terraspace::Compiler
2
+ class Select
3
+ def initialize(path)
4
+ @path = path
5
+ @stack_name = extract_stack_name(path)
6
+ end
7
+
8
+ def selected?
9
+ all = Terraspace.config.all
10
+ # Key difference between include_stacks vs all.include_stacks option is that
11
+ # the option can be nil. The local variable is guaranteed to be an Array.
12
+ # This simplifies the logic.
13
+ include_stacks = all.include_stacks || []
14
+ ignore_stacks = all.ignore_stacks || []
15
+
16
+ if all.include_stacks.nil?
17
+ !ignore_stacks.include?(@stack_name)
18
+ else
19
+ stacks = include_stacks - ignore_stacks
20
+ stacks.include?(@stack_name)
21
+ end
22
+ end
23
+
24
+ def extract_stack_name(path)
25
+ path.sub(%r{.*(app|vendor)/stacks/}, '')
26
+ end
27
+ end
28
+ end
@@ -20,18 +20,30 @@ module Terraspace::Compiler::Strategy
20
20
  end
21
21
  end
22
22
 
23
+ def layer_paths
24
+ Layer.new(@mod).paths
25
+ end
26
+
23
27
  # Tact on number to ensure that tfvars will be processed in desired order.
24
28
  # Also name auto.tfvars so it will automatically load
25
29
  def ordered_name(layer_path)
26
30
  @order += 1
27
- prefix = @order.to_s.rjust(2, '0') # add leading 0 in case there are more than 10 layers
28
- name = "#{prefix}-#{File.basename(layer_path)}"
31
+ prefix = @order.to_s
32
+ # add leading 0 when more than 10 layers
33
+ prefix = prefix.rjust(2, '0') if layer_paths.size > 9
34
+ name = "#{prefix}-#{tfvar_name(layer_path)}"
29
35
  name.sub('.tfvars','.auto.tfvars')
30
36
  .sub('.rb','.auto.tfvars.json')
31
37
  end
32
38
 
33
- def layer_paths
34
- Layer.new(@mod).paths
39
+ def tfvar_name(layer_path)
40
+ if layer_path.include?('/tfvars/')
41
+ name = layer_path.sub(%r{.*/tfvars/},'').gsub('/','-')
42
+ name = "project-#{name}" if layer_path.include?("config/terraform/tfvars")
43
+ name
44
+ else
45
+ File.basename(layer_path)
46
+ end
35
47
  end
36
48
 
37
49
  def strategy_class(ext)
@@ -1,11 +1,51 @@
1
+ # Layers in order
2
+ #
3
+ # Name / Pattern | Example
4
+ # -------------------------------|---------------
5
+ # base | base.tfvars
6
+ # env | dev.tfvars
7
+ # region/base | us-west-2/base.tfvars (provider specific)
8
+ # region/env | us-west-2/dev.tfvars (provider specific)
9
+ # namespace/base | 112233445566/base.tfvars (provider specific)
10
+ # namespace/env | 112233445566/dev.tfvars (provider specific)
11
+ # namespace/region/base | 112233445566/us-west-2/base.tfvars (provider specific)
12
+ # namespace/region/env | 112233445566/us-west-2/dev.tfvars (provider specific)
13
+ # provider/base | aws/base.tfvars (provider specific)
14
+ # provider/env | aws/dev.tfvars (provider specific)
15
+ # provider/region/base | aws/us-west-2/base.tfvars (provider specific)
16
+ # provider/region/env | aws/us-west-2/dev.tfvars (provider specific)
17
+ # provider/namespace/base | aws/112233445566/base.tfvars (provider specific)
18
+ # provider/namespace/env | aws/112233445566/dev.tfvars (provider specific)
19
+ # provider/namespace/region/base | aws/112233445566/us-west-2/base.tfvars (provider specific)
20
+ # provider/namespace/region/env | aws/112233445566/us-west-2/dev.tfvars (provider specific)
21
+ #
22
+ # namespace and region depends on the provider. Here an example of the mapping:
23
+ #
24
+ # | AWS | Azure | Google
25
+ # ----------|---------|--------------|-------
26
+ # namespace | account | subscription | project
27
+ # region | region | location | region
28
+ #
29
+ #
1
30
  class Terraspace::Compiler::Strategy::Tfvar
2
31
  class Layer
32
+ extend Memoist
33
+ include Terraspace::Layering
34
+ include Terraspace::Plugin::Expander::Friendly
35
+
3
36
  def initialize(mod)
4
37
  @mod = mod
5
38
  end
6
39
 
7
40
  def paths
8
- layer_paths = layers.map do |layer|
41
+ project_paths = full_paths(project_tfvars_dir)
42
+ stack_paths = full_paths(stack_tfvars_dir)
43
+ project_paths + stack_paths
44
+ end
45
+ memoize :paths
46
+
47
+ def full_paths(tfvars_dir)
48
+ layer_paths = full_layering.map do |layer|
9
49
  [
10
50
  "#{tfvars_dir}/#{layer}.tfvars",
11
51
  "#{tfvars_dir}/#{layer}.rb",
@@ -17,74 +57,58 @@ class Terraspace::Compiler::Strategy::Tfvar
17
57
  end
18
58
  end
19
59
 
20
- # Layers in order
21
- #
22
- # Name / Pattern | Example
23
- # -------------------------------|---------------
24
- # base | base.tfvars
25
- # env | dev.tfvars
26
- # region/base | us-west-2/base.tfvars (provider specific)
27
- # region/env | us-west-2/dev.tfvars (provider specific)
28
- # namespace/base | 112233445566/base.tfvars (provider specific)
29
- # namespace/env | 112233445566/dev.tfvars (provider specific)
30
- # namespace/region/base | 112233445566/us-west-2/base.tfvars (provider specific)
31
- # namespace/region/env | 112233445566/us-west-2/dev.tfvars (provider specific)
32
- # provider/base | aws/base.tfvars (provider specific)
33
- # provider/env | aws/dev.tfvars (provider specific)
34
- # provider/region/base | aws/us-west-2/base.tfvars (provider specific)
35
- # provider/region/env | aws/us-west-2/dev.tfvars (provider specific)
36
- # provider/namespace/base | aws/112233445566/base.tfvars (provider specific)
37
- # provider/namespace/env | aws/112233445566/dev.tfvars (provider specific)
38
- # provider/namespace/region/base | aws/112233445566/us-west-2/base.tfvars (provider specific)
39
- # provider/namespace/region/env | aws/112233445566/us-west-2/dev.tfvars (provider specific)
40
- #
41
- # namespace and region depends on the provider. Here an example of the mapping:
42
- #
43
- # | AWS | Azure | Google
44
- # ----------|---------|--------------|-------
45
- # namespace | account | subscription | project
46
- # region | region | location | region
60
+ def full_layering
61
+ # layers is defined in Terraspace::Layering module
62
+ layers.inject([]) do |sum, layer|
63
+ sum += layer_levels(layer) unless layer.nil?
64
+ sum
65
+ end
66
+ end
67
+
68
+ # adds prefix and to each layer pair that has base and Terraspace.env. IE:
47
69
  #
70
+ # "#{prefix}/base"
71
+ # "#{prefix}/#{Terraspace.env}"
48
72
  #
49
- def layers
50
- layer_levels + plugin_layers
73
+ def layer_levels(prefix=nil)
74
+ levels = ["base", Terraspace.env, @mod.instance].reject(&:blank?) # layer levels. @mod.instance can be nil
75
+ env_levels = levels.map { |l| "#{Terraspace.env}/#{l}" } # env folder also
76
+ levels = levels + env_levels
77
+ levels.map! do |i|
78
+ # base layer has prefix of '', reject with blank so it doesnt produce '//'
79
+ [prefix, i].reject(&:blank?).join('/')
80
+ end
81
+ levels.unshift(prefix) unless prefix.blank? # IE: tfvars/us-west-2.tfvars
82
+ levels
51
83
  end
52
84
 
53
- def plugin_layers
85
+ def plugins
54
86
  layers = []
55
87
  Terraspace::Plugin.layer_classes.each do |klass|
56
88
  layer = klass.new
57
89
 
58
90
  # region is high up because its simpler and the more common case is a single provider
59
- layers += layer_levels(layer.region)
91
+ layers << layer.region
92
+
93
+ namespace = friendly_name(layer.namespace)
60
94
 
61
95
  # namespace is a simple way keep different tfvars between different engineers on different accounts
62
- layers += layer_levels(layer.namespace)
63
- layers += layer_levels("#{layer.namespace}/#{layer.region}")
96
+ layers << namespace
97
+ layers << "#{namespace}/#{layer.region}"
64
98
 
65
99
  # in case using multiple providers and one region
66
- layers += layer_levels(layer.provider)
67
- layers += layer_levels("#{layer.provider}/#{layer.region}") # also in case another provider has colliding regions
100
+ layers << layer.provider
101
+ layers << "#{layer.provider}/#{layer.region}" # also in case another provider has colliding regions
68
102
 
69
103
  # Most general layering
70
- layers += layer_levels("#{layer.provider}/#{layer.namespace}")
71
- layers += layer_levels("#{layer.provider}/#{layer.namespace}/#{layer.region}")
104
+ layers << "#{layer.provider}/#{namespace}"
105
+ layers << "#{layer.provider}/#{namespace}/#{layer.region}"
72
106
  end
73
107
  layers
74
108
  end
75
109
 
76
- # adds prefix and to each layer pair that has base and Terraspace.env. IE:
77
- #
78
- # "#{prefix}/base"
79
- # "#{prefix}/#{Terraspace.env}"
80
- #
81
- def layer_levels(prefix=nil)
82
- levels = ["base", Terraspace.env, @mod.instance] # layer levels
83
- env_levels = levels.map { |l| "#{Terraspace.env}/#{l}" } # env folder also
84
- levels = levels + env_levels
85
- levels.map do |i|
86
- [prefix, i].compact.join('/')
87
- end
110
+ def project_tfvars_dir
111
+ "#{Terraspace.root}/config/terraform/tfvars"
88
112
  end
89
113
 
90
114
  # seed dir takes higher precedence than the tfvars folder within the stack module. Example:
@@ -98,13 +122,12 @@ class Terraspace::Compiler::Strategy::Tfvar
98
122
  # Will also consider app/modules/demo/tfvars. Though modules to be reuseable and stacks is where business logic
99
123
  # should go.
100
124
  #
101
- def tfvars_dir
125
+ def stack_tfvars_dir
102
126
  seed_dir = "#{Terraspace.root}/seed/tfvars/#{@mod.build_dir(disable_instance: true)}"
103
127
  mod_dir = "#{@mod.root}/tfvars"
104
128
 
105
129
  empty = Dir.glob("#{seed_dir}/*").empty?
106
130
  empty ? mod_dir : seed_dir
107
131
  end
108
-
109
132
  end
110
133
  end
@@ -0,0 +1,24 @@
1
+ require "active_support/lazy_load_hooks"
2
+
3
+ module Terraspace
4
+ module Layering
5
+ def layers
6
+ pre_layers + main_layers + post_layers
7
+ end
8
+
9
+ def main_layers
10
+ # '' prefix for base layer
11
+ [''] + plugins
12
+ end
13
+
14
+ def pre_layers
15
+ []
16
+ end
17
+
18
+ def post_layers
19
+ []
20
+ end
21
+ end
22
+ end
23
+
24
+ ActiveSupport.run_load_hooks(:terraspace_layering, Terraspace::Layering)