terraspace 0.5.12 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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