terraspace 0.5.12 → 0.6.0

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -7
  3. data/README.md +2 -0
  4. data/lib/terraspace/app.rb +34 -13
  5. data/lib/terraspace/app/inits.rb +13 -0
  6. data/lib/terraspace/booter.rb +1 -0
  7. data/lib/terraspace/cli.rb +12 -5
  8. data/lib/terraspace/cli/commander.rb +1 -1
  9. data/lib/terraspace/cli/down.rb +1 -1
  10. data/lib/terraspace/cli/help/{cloud → tfc}/destroy.md +1 -1
  11. data/lib/terraspace/cli/help/{cloud → tfc}/list.md +1 -1
  12. data/lib/terraspace/cli/help/{cloud → tfc}/runs/list.md +3 -3
  13. data/lib/terraspace/cli/help/{cloud → tfc}/runs/prune.md +3 -3
  14. data/lib/terraspace/cli/help/{cloud → tfc}/sync.md +3 -3
  15. data/lib/terraspace/cli/init.rb +21 -8
  16. data/lib/terraspace/cli/state.rb +10 -0
  17. data/lib/terraspace/cli/{cloud.rb → tfc.rb} +3 -3
  18. data/lib/terraspace/cli/{cloud → tfc}/runs.rb +4 -4
  19. data/lib/terraspace/compiler/builder.rb +2 -0
  20. data/lib/terraspace/compiler/strategy/tfvar.rb +16 -4
  21. data/lib/terraspace/compiler/strategy/tfvar/layer.rb +75 -52
  22. data/lib/terraspace/layering.rb +24 -0
  23. data/lib/terraspace/logger.rb +8 -1
  24. data/lib/terraspace/mod.rb +18 -3
  25. data/lib/terraspace/plugin/expander/friendly.rb +10 -0
  26. data/lib/terraspace/plugin/expander/interface.rb +6 -1
  27. data/lib/terraspace/plugin/summary/interface.rb +1 -1
  28. data/lib/terraspace/shell.rb +16 -1
  29. data/lib/terraspace/terraform/api/runs.rb +13 -2
  30. data/lib/terraspace/terraform/api/token.rb +2 -2
  31. data/lib/terraspace/terraform/api/var.rb +1 -1
  32. data/lib/terraspace/terraform/api/vars.rb +1 -1
  33. data/lib/terraspace/terraform/api/vars/base.rb +2 -0
  34. data/lib/terraspace/terraform/api/vars/json.rb +13 -1
  35. data/lib/terraspace/terraform/api/workspace.rb +10 -3
  36. data/lib/terraspace/terraform/args/default.rb +23 -13
  37. data/lib/terraspace/terraform/ihooks/after/plan.rb +17 -0
  38. data/lib/terraspace/terraform/ihooks/base.rb +8 -0
  39. data/lib/terraspace/terraform/ihooks/before/plan.rb +14 -0
  40. data/lib/terraspace/terraform/remote_state/fetcher.rb +1 -1
  41. data/lib/terraspace/terraform/runner.rb +12 -0
  42. data/lib/terraspace/terraform/{cloud → tfc}/runs.rb +1 -1
  43. data/lib/terraspace/terraform/{cloud → tfc}/runs/base.rb +1 -1
  44. data/lib/terraspace/terraform/{cloud → tfc}/runs/item_presenter.rb +1 -1
  45. data/lib/terraspace/terraform/{cloud → tfc}/runs/lister.rb +1 -1
  46. data/lib/terraspace/terraform/{cloud → tfc}/runs/pruner.rb +1 -1
  47. data/lib/terraspace/terraform/{cloud → tfc}/sync.rb +2 -2
  48. data/lib/terraspace/terraform/{cloud → tfc}/syncer.rb +1 -1
  49. data/lib/terraspace/terraform/{cloud → tfc}/workspace.rb +2 -3
  50. data/lib/terraspace/util/pretty.rb +2 -1
  51. data/lib/terraspace/version.rb +1 -1
  52. metadata +24 -17
@@ -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")
@@ -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
 
@@ -0,0 +1,10 @@
1
+ module Terraspace::Plugin::Expander
2
+ module Friendly
3
+ # used by
4
+ # Terraspace::Compiler::Strategy::Tfvar::Layer
5
+ # Terraspace::Plugin::Expander::Interface
6
+ def friendly_name(name)
7
+ Terraspace.config.layering.names[name.to_sym] || name
8
+ end
9
+ end
10
+ end
@@ -7,6 +7,7 @@
7
7
  module Terraspace::Plugin::Expander
8
8
  module Interface
9
9
  include Terraspace::Plugin::InferProvider
10
+ include Terraspace::Plugin::Expander::Friendly
10
11
 
11
12
  delegate :build_dir, :type_dir, :type, to: :mod
12
13
 
@@ -68,7 +69,11 @@ module Terraspace::Plugin::Expander
68
69
 
69
70
  def var_value(name)
70
71
  name = name.sub(':','').downcase
71
- send(name)
72
+ value = send(name)
73
+ if name == "namespace" && Terraspace.config.layering.enable_names.cache_dir
74
+ value = friendly_name(value)
75
+ end
76
+ value
72
77
  end
73
78
 
74
79
  def mod_name
@@ -67,7 +67,7 @@ module Terraspace::Plugin::Summary
67
67
  return unless data # edge case: blank file
68
68
  resources = data['resources']
69
69
  return unless resources
70
- remove_statefile(path) if resources && resources.size == 0 && !ENV['TS_KEEP_EMPTY_STATEFILES']
70
+ remove_statefile(path) if Terraspace.config.summary.prune && resources && resources.size == 0
71
71
  return unless resources && resources.size > 0
72
72
 
73
73
  pretty_path = path.sub(Regexp.new(".*#{@bucket}/#{@folder}"), '')
@@ -30,6 +30,10 @@ module Terraspace
30
30
  def popen3(env)
31
31
  Open3.popen3(env, @command, chdir: @mod.cache_dir) do |stdin, stdout, stderr, wait_thread|
32
32
  mimic_terraform_input(stdin, stdout)
33
+ while out = stdout.gets
34
+ terraform_to_stdout(out)
35
+ end
36
+
33
37
  while err = stderr.gets
34
38
  @error ||= Error.new
35
39
  @error.lines << err # aggregate all error lines
@@ -69,7 +73,7 @@ module Terraspace
69
73
  "\e[0m\e[1mvar.", # prompts for variable input. can happen on plan or apply. looking for bold marker also in case "var." shows up somewhere else
70
74
  ]
71
75
  while out = stdout.gets
72
- logger.info(out) unless shown && out.include?("Enter a value:")
76
+ terraform_to_stdout(out) unless shown && out.include?("Enter a value:")
73
77
  shown = false if out.include?("Enter a value:") # reset shown in case of multiple input prompts
74
78
 
75
79
  # Sometimes stdout doesnt flush and show "Enter a value: ", so mimic it
@@ -80,5 +84,16 @@ module Terraspace
80
84
  end
81
85
  end
82
86
  end
87
+
88
+ # Allows piping to jq. IE:
89
+ # terraspace show demo --json | jq
90
+ def terraform_to_stdout(out)
91
+ # so terraform output goes stdout
92
+ if logger.respond_to?(:stdout) && !@options[:log_to_stderr]
93
+ logger.stdout(out)
94
+ else
95
+ logger.info(out)
96
+ end
97
+ end
83
98
  end
84
99
  end
@@ -8,8 +8,19 @@ class Terraspace::Terraform::Api
8
8
  end
9
9
 
10
10
  def list
11
- payload = http.get("workspaces/#{@workspace_id}/runs")
12
- payload['data'] if payload
11
+ data, next_page = [], :start
12
+ while next_page == :start || next_page
13
+ url = "workspaces/#{@workspace_id}/runs"
14
+ if next_page
15
+ qs = URI.encode_www_form('page[number]': next_page) if next_page
16
+ url += "?#{qs}"
17
+ end
18
+ payload = http.get(url)
19
+ return unless payload
20
+ data += payload['data']
21
+ next_page = payload['meta']['pagination']['next-page']
22
+ end
23
+ data
13
24
  end
14
25
 
15
26
  def discard(id)
@@ -51,11 +51,11 @@ class Terraspace::Terraform::Api
51
51
  end
52
52
 
53
53
  def hostname
54
- ENV['TS_HOST'] || Terraspace.config.cloud.hostname || 'app.terraform.io'
54
+ ENV['TFC_HOST'] || Terraspace.config.tfc.hostname || 'app.terraform.io'
55
55
  end
56
56
 
57
57
  def hostname_configured?
58
- !!Terraspace.config.cloud.hostname
58
+ !!Terraspace.config.tfc.hostname
59
59
  end
60
60
 
61
61
  def self.get
@@ -31,7 +31,7 @@ class Terraspace::Terraform::Api
31
31
  end
32
32
 
33
33
  def vars
34
- Terraspace.config.cloud.vars
34
+ Terraspace.config.tfc.vars
35
35
  end
36
36
 
37
37
  def variable_id(key)
@@ -33,7 +33,7 @@ class Terraspace::Terraform::Api
33
33
 
34
34
  def vars_path
35
35
  # .rb takes higher precedence
36
- Dir.glob("#{Terraspace.root}/config/terraform/cloud/vars.{rb,json}").first
36
+ Dir.glob("#{Terraspace.root}/config/terraform/tfc/vars.{rb,json}").first
37
37
  end
38
38
  end
39
39
  end