terraspace 0.5.9 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +29 -7
  6. data/README.md +2 -0
  7. data/lib/terraspace.rb +1 -0
  8. data/lib/terraspace/app.rb +36 -16
  9. data/lib/terraspace/app/inits.rb +13 -0
  10. data/lib/terraspace/booter.rb +1 -0
  11. data/lib/terraspace/cli.rb +13 -6
  12. data/lib/terraspace/cli/build/placeholder.rb +6 -1
  13. data/lib/terraspace/cli/commander.rb +1 -1
  14. data/lib/terraspace/cli/down.rb +1 -1
  15. data/lib/terraspace/cli/help/{cloud → tfc}/destroy.md +1 -1
  16. data/lib/terraspace/cli/help/{cloud → tfc}/list.md +1 -1
  17. data/lib/terraspace/cli/help/{cloud → tfc}/runs/list.md +3 -3
  18. data/lib/terraspace/cli/help/{cloud → tfc}/runs/prune.md +3 -3
  19. data/lib/terraspace/cli/help/{cloud → tfc}/sync.md +3 -3
  20. data/lib/terraspace/cli/init.rb +21 -8
  21. data/lib/terraspace/cli/state.rb +10 -0
  22. data/lib/terraspace/cli/{cloud.rb → tfc.rb} +3 -3
  23. data/lib/terraspace/cli/{cloud → tfc}/runs.rb +4 -4
  24. data/lib/terraspace/cli/up.rb +5 -3
  25. data/lib/terraspace/compiler/builder.rb +2 -0
  26. data/lib/terraspace/compiler/dirs_concern.rb +11 -1
  27. data/lib/terraspace/compiler/select.rb +28 -0
  28. data/lib/terraspace/compiler/strategy/tfvar.rb +16 -4
  29. data/lib/terraspace/compiler/strategy/tfvar/layer.rb +75 -52
  30. data/lib/terraspace/layering.rb +24 -0
  31. data/lib/terraspace/logger.rb +8 -1
  32. data/lib/terraspace/mod.rb +18 -3
  33. data/lib/terraspace/plugin/expander/friendly.rb +10 -0
  34. data/lib/terraspace/plugin/expander/interface.rb +6 -1
  35. data/lib/terraspace/plugin/summary/interface.rb +1 -1
  36. data/lib/terraspace/shell.rb +16 -1
  37. data/lib/terraspace/shell/error.rb +1 -1
  38. data/lib/terraspace/terraform/api/runs.rb +13 -2
  39. data/lib/terraspace/terraform/api/token.rb +2 -2
  40. data/lib/terraspace/terraform/api/var.rb +1 -1
  41. data/lib/terraspace/terraform/api/vars.rb +1 -1
  42. data/lib/terraspace/terraform/api/vars/base.rb +2 -0
  43. data/lib/terraspace/terraform/api/vars/json.rb +13 -1
  44. data/lib/terraspace/terraform/api/workspace.rb +10 -3
  45. data/lib/terraspace/terraform/args/default.rb +23 -13
  46. data/lib/terraspace/terraform/ihooks/after/plan.rb +17 -0
  47. data/lib/terraspace/terraform/ihooks/base.rb +8 -0
  48. data/lib/terraspace/terraform/ihooks/before/plan.rb +14 -0
  49. data/lib/terraspace/terraform/remote_state/fetcher.rb +1 -1
  50. data/lib/terraspace/terraform/runner.rb +12 -0
  51. data/lib/terraspace/terraform/{cloud → tfc}/runs.rb +1 -1
  52. data/lib/terraspace/terraform/{cloud → tfc}/runs/base.rb +1 -1
  53. data/lib/terraspace/terraform/{cloud → tfc}/runs/item_presenter.rb +1 -1
  54. data/lib/terraspace/terraform/{cloud → tfc}/runs/lister.rb +1 -1
  55. data/lib/terraspace/terraform/{cloud → tfc}/runs/pruner.rb +1 -1
  56. data/lib/terraspace/terraform/{cloud → tfc}/sync.rb +2 -2
  57. data/lib/terraspace/terraform/{cloud → tfc}/syncer.rb +1 -1
  58. data/lib/terraspace/terraform/{cloud → tfc}/workspace.rb +2 -3
  59. data/lib/terraspace/util/pretty.rb +2 -1
  60. data/lib/terraspace/version.rb +1 -1
  61. metadata +26 -18
@@ -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)
@@ -5,7 +5,7 @@ module Terraspace
5
5
  def initialize(*args)
6
6
  super
7
7
  self.formatter = Formatter.new
8
- self.level = :info
8
+ self.level = ENV['TS_LOG_LEVEL'] || :info # note: only respected when config.logger not set in config/app.rb
9
9
  end
10
10
 
11
11
  def format_message(severity, datetime, progname, msg)
@@ -16,5 +16,12 @@ module Terraspace
16
16
  end
17
17
  line =~ /\n$/ ? line : "#{line}\n"
18
18
  end
19
+
20
+ # Used to allow terraform output to always go to stdout
21
+ # Terraspace output goes to stderr by default
22
+ # See: terraspace/shell.rb
23
+ def stdout(msg)
24
+ puts msg
25
+ end
19
26
  end
20
27
  end
@@ -75,7 +75,7 @@ module Terraspace
75
75
  #
76
76
  # down - so user can delete stacks w/o needing to create an empty app/stacks/demo folder
77
77
  # null - for the terraspace summary command when there are zero stacks.
78
- # Also useful for terraspace cloud list_workspaces
78
+ # Also useful for terraspace tfc list_workspaces
79
79
  #
80
80
  def possible_fake_root
81
81
  if @options[:command] == "down"
@@ -111,9 +111,24 @@ module Terraspace
111
111
 
112
112
  # Full path with build_dir
113
113
  def cache_dir
114
- pattern = Terraspace.config.build.cache_dir # IE: :CACHE_ROOT/:REGION/:ENV/:BUILD_DIR
114
+ # config.build.cache_dir is a String or object that respond_to call. IE:
115
+ # :CACHE_ROOT/:REGION/:ENV/:BUILD_DIR
116
+ # CustomBuildDir.call
117
+ # The call method should return a String pattern used for substitutions
118
+ object = Terraspace.config.build.cache_dir
119
+ pattern = if object.is_a?(String)
120
+ object
121
+ elsif object.respond_to?(:call)
122
+ object.call(self)
123
+ elsif object.public_instance_methods.include?(:call)
124
+ instance = object.new
125
+ instance.call(self)
126
+ else
127
+ raise "ERROR: config.build.cache_dir is not a String or responds to the .call method."
128
+ end
129
+
115
130
  expander = Terraspace::Compiler::Expander.autodetect(self)
116
- expander.expansion(pattern)
131
+ expander.expansion(pattern) # pattern is a String that contains placeholders for substitutions
117
132
  end
118
133
  memoize :cache_dir
119
134