terraspace 0.5.9 → 0.6.1

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 +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