terraspace 0.2.3 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +14 -1
  4. data/lib/templates/base/git_hook/hook.sh +1 -1
  5. data/lib/templates/base/project/.gitignore +1 -0
  6. data/lib/templates/base/project/README.md +17 -0
  7. data/lib/terraspace.rb +5 -0
  8. data/lib/terraspace/all/base.rb +8 -0
  9. data/lib/terraspace/all/grapher.rb +129 -0
  10. data/lib/terraspace/all/preview.rb +43 -0
  11. data/lib/terraspace/all/runner.rb +169 -0
  12. data/lib/terraspace/all/summary.rb +99 -0
  13. data/lib/terraspace/app.rb +31 -9
  14. data/lib/terraspace/booter.rb +9 -0
  15. data/lib/terraspace/builder.rb +59 -22
  16. data/lib/terraspace/cli.rb +39 -12
  17. data/lib/terraspace/cli/all.rb +63 -0
  18. data/lib/terraspace/cli/build/placeholder.rb +2 -5
  19. data/lib/terraspace/cli/bundle.rb +1 -1
  20. data/lib/terraspace/cli/check_setup.rb +17 -5
  21. data/lib/terraspace/cli/cloud.rb +18 -2
  22. data/lib/terraspace/cli/cloud/runs.rb +24 -0
  23. data/lib/terraspace/cli/commander.rb +1 -8
  24. data/lib/terraspace/cli/down.rb +20 -0
  25. data/lib/terraspace/cli/help/cloud/runs/list.md +36 -0
  26. data/lib/terraspace/cli/help/cloud/runs/prune.md +25 -0
  27. data/lib/terraspace/cli/help/cloud/sync.md +19 -0
  28. data/lib/terraspace/cli/help/log.md +46 -0
  29. data/lib/terraspace/cli/init.rb +35 -7
  30. data/lib/terraspace/cli/list.rb +14 -1
  31. data/lib/terraspace/cli/log.rb +112 -0
  32. data/lib/terraspace/cli/log/concern.rb +24 -0
  33. data/lib/terraspace/cli/logs.rb +15 -0
  34. data/lib/terraspace/cli/logs/tasks.rb +32 -0
  35. data/lib/terraspace/cli/new/git_hook.rb +1 -1
  36. data/lib/terraspace/cli/tfc_concern.rb +14 -0
  37. data/lib/terraspace/cli/up.rb +32 -0
  38. data/lib/terraspace/compiler/backend.rb +10 -0
  39. data/lib/terraspace/compiler/builder.rb +5 -4
  40. data/lib/terraspace/compiler/cleaner.rb +1 -1
  41. data/lib/terraspace/compiler/cleaner/backend_change.rb +21 -7
  42. data/lib/terraspace/compiler/commands_concern.rb +18 -0
  43. data/lib/terraspace/compiler/dirs_concern.rb +47 -0
  44. data/lib/terraspace/compiler/dsl/syntax/helpers/common.rb +26 -1
  45. data/lib/terraspace/core.rb +11 -2
  46. data/lib/terraspace/dependency/graph.rb +139 -0
  47. data/lib/terraspace/dependency/node.rb +38 -0
  48. data/lib/terraspace/dependency/registry.rb +11 -0
  49. data/lib/terraspace/logger.rb +6 -18
  50. data/lib/terraspace/logger/formatter.rb +13 -0
  51. data/lib/terraspace/mod.rb +7 -1
  52. data/lib/terraspace/seeder/where.rb +6 -2
  53. data/lib/terraspace/shell.rb +97 -0
  54. data/lib/terraspace/terraform/api.rb +7 -45
  55. data/lib/terraspace/terraform/api/base.rb +7 -0
  56. data/lib/terraspace/terraform/api/client.rb +23 -3
  57. data/lib/terraspace/terraform/api/http.rb +14 -34
  58. data/lib/terraspace/terraform/api/http/concern.rb +10 -0
  59. data/lib/terraspace/terraform/api/runs.rb +28 -0
  60. data/lib/terraspace/terraform/api/token.rb +65 -0
  61. data/lib/terraspace/terraform/api/var.rb +20 -6
  62. data/lib/terraspace/terraform/api/vars.rb +2 -1
  63. data/lib/terraspace/terraform/api/workspace.rb +98 -0
  64. data/lib/terraspace/terraform/args/default.rb +48 -21
  65. data/lib/terraspace/terraform/cloud/runs.rb +13 -0
  66. data/lib/terraspace/terraform/cloud/runs/base.rb +33 -0
  67. data/lib/terraspace/terraform/cloud/runs/item_presenter.rb +37 -0
  68. data/lib/terraspace/terraform/cloud/runs/lister.rb +22 -0
  69. data/lib/terraspace/terraform/cloud/runs/pruner.rb +109 -0
  70. data/lib/terraspace/terraform/cloud/sync.rb +41 -0
  71. data/lib/terraspace/terraform/cloud/syncer.rb +52 -0
  72. data/lib/terraspace/terraform/cloud/workspace.rb +10 -21
  73. data/lib/terraspace/terraform/hooks/builder.rb +1 -1
  74. data/lib/terraspace/terraform/remote_state/fetcher.rb +143 -0
  75. data/lib/terraspace/terraform/remote_state/marker/output.rb +39 -0
  76. data/lib/terraspace/terraform/remote_state/marker/pretty_tracer.rb +37 -0
  77. data/lib/terraspace/terraform/remote_state/output_proxy.rb +29 -0
  78. data/lib/terraspace/terraform/runner.rb +24 -14
  79. data/lib/terraspace/util.rb +1 -5
  80. data/lib/terraspace/util/pretty.rb +18 -0
  81. data/lib/terraspace/version.rb +1 -1
  82. data/spec/fixtures/fetcher/c1.json +37 -0
  83. data/spec/fixtures/parser/cache_dirs/all/01-test.auto.tfvars +5 -0
  84. data/spec/fixtures/parser/cache_dirs/depends_on/01-test.auto.tfvars +2 -0
  85. data/spec/fixtures/parser/cache_dirs/output/01-test.auto.tfvars +2 -0
  86. data/spec/fixtures/summary/down.log +12 -0
  87. data/spec/fixtures/summary/output.log +5 -0
  88. data/spec/fixtures/summary/plan/error.log +20 -0
  89. data/spec/fixtures/summary/plan/success.log +17 -0
  90. data/spec/fixtures/summary/show.log +22 -0
  91. data/spec/fixtures/summary/up/error.log +13 -0
  92. data/spec/fixtures/summary/up/success.log +63 -0
  93. data/spec/fixtures/summary/validate/error.log +13 -0
  94. data/spec/fixtures/summary/validate/success.log +5 -0
  95. data/spec/terraspace/all/grapher_spec.rb +38 -0
  96. data/spec/terraspace/all/runner_spec.rb +48 -0
  97. data/spec/terraspace/all/summary_spec.rb +93 -0
  98. data/spec/terraspace/dependency/graph_spec.rb +162 -0
  99. data/spec/terraspace/seeder_spec.rb +0 -1
  100. data/spec/terraspace/terraform/remote_state/fetcher_spec.rb +52 -0
  101. data/terraspace.gemspec +5 -1
  102. metadata +138 -5
  103. data/lib/terraspace/terraform/cloud.rb +0 -25
  104. data/lib/terraspace/util/sh.rb +0 -19
@@ -0,0 +1,65 @@
1
+ class Terraspace::Terraform::Api
2
+ class Token
3
+ include Terraspace::Util::Logging
4
+
5
+ attr_reader :token
6
+ def initialize
7
+ @creds_path = "#{ENV['HOME']}/.terraform.d/credentials.tfrc.json"
8
+ @hostname = hostname
9
+ end
10
+
11
+ def get
12
+ @token = ENV['TERRAFORM_TOKEN']
13
+ return @token if @token
14
+ @token = load
15
+ return @token if @token
16
+ error_exit!
17
+ end
18
+
19
+ def load
20
+ return unless File.exist?(@creds_path)
21
+
22
+ data = JSON.load(IO.read(@creds_path))
23
+ @token = data.dig('credentials', @hostname, 'token')
24
+ return @token if @token
25
+
26
+ return unless hostname_configured?
27
+ logger.error "You configured a cloud.hostname: #{@hostname}".color(:red)
28
+ logger.error <<~EOL
29
+ But it was not found into your #{@creds_path}
30
+ Please double check it.
31
+ EOL
32
+ @token
33
+ end
34
+
35
+ # Internal note only way to get here is to bypass init. Example:
36
+ #
37
+ # terraspace up demo --no-init
38
+ #
39
+ def error_exit!
40
+ login_hostname = @hostname if hostname_configured?
41
+ logger.error "ERROR: Unable to not find a Terraform token. A Terraform token is needed for Terraspace to call the Terraform API.".color(:red)
42
+ logger.error <<~EOL
43
+ Here are some ways to provide the Terraform token:
44
+
45
+ 1. By running: terraform login #{login_hostname}
46
+ 2. With an env variable: export TERRAFORM_TOKEN=xxx
47
+
48
+ Please configure a Terraform token and try again.
49
+ EOL
50
+ exit 1
51
+ end
52
+
53
+ def hostname
54
+ ENV['TS_HOST'] || Terraspace.config.cloud.hostname || 'app.terraform.io'
55
+ end
56
+
57
+ def hostname_configured?
58
+ !!Terraspace.config.cloud.hostname
59
+ end
60
+
61
+ def self.get
62
+ new.get
63
+ end
64
+ end
65
+ end
@@ -1,9 +1,10 @@
1
1
  class Terraspace::Terraform::Api
2
2
  class Var
3
3
  extend Memoist
4
- include Client
4
+ include Http::Concern
5
5
  include Terraspace::Util::Logging
6
6
 
7
+ # workspace: details from the api response
7
8
  def initialize(workspace, attrs={})
8
9
  @workspace, @attrs = workspace, attrs
9
10
  @workspace_id = @workspace['id']
@@ -15,27 +16,30 @@ class Terraspace::Terraform::Api
15
16
 
16
17
  def update
17
18
  return unless overwrite?
18
- logger.debug "Updating Terraform Cloud #{category} variable: #{@attrs['key']}"
19
+ updating_message
19
20
  variable_id = variable_id(@attrs['key'])
20
21
  payload = payload(variable_id)
21
22
  http.patch("workspaces/#{@workspace_id}/vars/#{variable_id}", payload)
22
23
  end
23
24
 
24
25
  def overwrite?
25
- cloud = Terraspace.config.cloud
26
26
  if @attrs['sensitive']
27
- cloud.overwrite_sensitive
27
+ vars.overwrite_sensitive
28
28
  else
29
- cloud.overwrite
29
+ vars.overwrite
30
30
  end
31
31
  end
32
32
 
33
+ def vars
34
+ Terraspace.config.cloud.vars
35
+ end
36
+
33
37
  def variable_id(key)
34
38
  current_var_resp['id']
35
39
  end
36
40
 
37
41
  def create
38
- logger.info "Creating Terraform Cloud #{category} variable: #{@attrs['key']}"
42
+ creating_message
39
43
  http.post("workspaces/#{@workspace_id}/vars", payload)
40
44
  end
41
45
 
@@ -48,6 +52,16 @@ class Terraspace::Terraform::Api
48
52
  { data: data }
49
53
  end
50
54
 
55
+ def updating_message
56
+ return unless %w[all update].include?(vars.show_message)
57
+ logger.info "Updating Terraform Cloud #{category} variable: #{@attrs['key']}"
58
+ end
59
+
60
+ def creating_message
61
+ return unless %w[all create].include?(vars.show_message)
62
+ logger.info "Creating Terraform Cloud #{category} variable: #{@attrs['key']}"
63
+ end
64
+
51
65
  def exist?
52
66
  !!current_var_resp
53
67
  end
@@ -1,8 +1,9 @@
1
1
  class Terraspace::Terraform::Api
2
2
  class Vars
3
3
  extend Memoist
4
- include Client
4
+ include Http::Concern
5
5
 
6
+ # workspace: details from the api response
6
7
  def initialize(mod, workspace)
7
8
  @mod, @workspace = mod, workspace
8
9
  end
@@ -0,0 +1,98 @@
1
+ class Terraspace::Terraform::Api
2
+ class Workspace < Base
3
+ extend Memoist
4
+
5
+ attr_reader :name
6
+ def initialize(mod, organization, name)
7
+ @mod, @organization, @name = mod, organization, name
8
+ end
9
+
10
+ # Docs: https://www.terraform.io/docs/cloud/api/workspaces.html
11
+ def set_working_dir
12
+ return if working_directory == details['attributes']['working-directory']
13
+
14
+ payload = {
15
+ data: {
16
+ attributes: {
17
+ "working-directory": working_directory
18
+ },
19
+ type: "workspaces"
20
+ }
21
+ }
22
+ http.patch("organizations/#{@organization}/workspaces/#{@name}", payload)
23
+ end
24
+
25
+ def working_directory
26
+ cache_dir = @mod.cache_dir.sub("#{Terraspace.root}/", '')
27
+ prefix = Terraspace.config.cloud.working_dir_prefix # prepended to TFC Working Directory
28
+ prefix ? "#{prefix}/#{cache_dir}" : cache_dir
29
+ end
30
+
31
+ def set_env_vars
32
+ Vars.new(@mod, details).run
33
+ end
34
+
35
+ def details(options={})
36
+ payload = http.get("organizations/#{@organization}/workspaces/#{@name}")
37
+ # Note only way to get here is to bypass init. Example:
38
+ #
39
+ # terraspace up demo --no-init
40
+ #
41
+ unless payload || options[:exit_on_fail] == false
42
+ logger.error "ERROR: Unable to find the workspace: #{@name}. The workspace may not exist. Or the Terraform token may be invalid. Please double check your Terraform token.".color(:red)
43
+ exit 1
44
+ end
45
+ payload['data'] if payload
46
+ end
47
+ memoize :details
48
+
49
+ def destroy
50
+ # response payload from delete operation is nil
51
+ http.delete("/organizations/#{@organization}/workspaces/#{@name}")
52
+ end
53
+
54
+ # Docs: https://www.terraform.io/docs/cloud/api/workspaces.html
55
+ def create
56
+ payload = upsert_payload
57
+ http.post("organizations/#{@organization}/workspaces", payload)
58
+ end
59
+
60
+ def update
61
+ payload = upsert_payload
62
+ http.patch("organizations/#{@organization}/workspaces/#{@name}", payload)
63
+ self.flush_cache
64
+ end
65
+
66
+ def upsert_payload
67
+ {
68
+ data: {
69
+ attributes: attributes,
70
+ type: "workspaces"
71
+ }
72
+ }
73
+ end
74
+
75
+ def attributes
76
+ attrs = { name: @name }
77
+ config = Terraspace.config.cloud.workspace.attrs
78
+ attrs.merge!(config)
79
+ # Default: run on all changes since app/modules can affect app/stacks
80
+ if config['vcs-repo'] && config['file-triggers-enabled'].nil?
81
+ attrs['file-triggers-enabled'.to_sym] = false
82
+ end
83
+ token = ENV['TS_CLOUD_OAUTH_TOKEN']
84
+ if config['vcs-repo'] && !config.dig('vcs-repo', 'oauth-token-id') && token
85
+ attrs['vcs-repo'.to_sym]['oauth-token-id'.to_sym] ||= token
86
+ end
87
+ attrs
88
+ end
89
+
90
+ def create_or_update
91
+ exist? ? update : create
92
+ end
93
+
94
+ def exist?
95
+ !!details(exit_on_fail: false)
96
+ end
97
+ end
98
+ end
@@ -11,7 +11,7 @@ module Terraspace::Terraform::Args
11
11
  # https://terraspace.cloud/docs/ci-automation/
12
12
  ENV['TF_IN_AUTOMATION'] = '1' if @options[:auto]
13
13
 
14
- if %w[apply destroy init output plan].include?(@name)
14
+ if %w[apply destroy init output plan show].include?(@name)
15
15
  meth = "#{@name}_args"
16
16
  send(meth)
17
17
  else
@@ -26,13 +26,7 @@ module Terraspace::Terraform::Args
26
26
  args << var_files.map { |f| "-var-file #{Dir.pwd}/#{f}" }.join(' ')
27
27
  end
28
28
 
29
- if @options[:auto] && @options[:input].nil?
30
- args << " -input=false"
31
- end
32
- unless @options[:input].nil?
33
- input = @options[:input] ? "true" : "false"
34
- args << " -input=#{input}" # = sign required for apply when there's a plan at the end. so input=false works input false doesnt
35
- end
29
+ args << input_option if input_option
36
30
 
37
31
  # must be at the end
38
32
  plan = @options[:plan]
@@ -44,12 +38,24 @@ module Terraspace::Terraform::Args
44
38
  src = "#{Dir.pwd}/#{plan}"
45
39
  dest = "#{@mod.cache_dir}/#{File.basename(src)}"
46
40
  end
47
- FileUtils.cp(src, dest)
41
+ FileUtils.cp(src, dest) unless same_file?(src, dest)
48
42
  args << " #{dest}"
49
43
  end
50
44
  args
51
45
  end
52
46
 
47
+ def input_option
48
+ option = nil
49
+ if @options[:auto] && @options[:input].nil?
50
+ option = " -input=false"
51
+ end
52
+ unless @options[:input].nil?
53
+ input = @options[:input] ? "true" : "false"
54
+ option = " -input=#{input}" # = sign required for apply when there's a plan at the end. so input=false works input false doesnt
55
+ end
56
+ option
57
+ end
58
+
53
59
  def init_args
54
60
  args = "-get"
55
61
  if @options[:auto] && @options[:input].nil?
@@ -63,24 +69,33 @@ module Terraspace::Terraform::Args
63
69
  args << " -reconfigure" if @options[:reconfigure]
64
70
 
65
71
  # must be at the end
66
- if @quiet && !ENV['TS_INIT_LOUD']
67
- out_path = self.class.terraform_init_log
68
- FileUtils.mkdir_p(File.dirname(out_path))
69
- args << " > #{out_path}"
72
+ if @quiet
73
+ log_path = self.class.terraform_init_log(@mod.name)
74
+ FileUtils.mkdir_p(File.dirname(log_path))
75
+ args << " >> #{log_path}"
70
76
  end
71
77
  [args]
72
78
  end
73
79
 
80
+ def output_args
81
+ args = []
82
+ args << "-json" if @options[:format] == "json"
83
+ args << "> #{expanded_out}" if @options[:out]
84
+ args
85
+ end
86
+
74
87
  def plan_args
75
88
  args = []
89
+ args << input_option if input_option
90
+ args << "-destroy" if @options[:destroy]
76
91
  args << "-out #{expanded_out}" if @options[:out]
77
92
  args
78
93
  end
79
94
 
80
- def output_args
95
+ def show_args
81
96
  args = []
82
- args << "-json" if @options[:format] == "json"
83
- args << "> #{expanded_out}" if @options[:out]
97
+ args << " -json" if @options[:json]
98
+ args << " #{@options[:plan]}" if @options[:plan] # terraform show /path/to/plan
84
99
  args
85
100
  end
86
101
 
@@ -98,13 +113,25 @@ module Terraspace::Terraform::Args
98
113
  end
99
114
 
100
115
  class << self
116
+ extend Memoist
117
+
101
118
  # Use different tmp log file in case uses run terraspace up in 2 terminals at the same time
102
- @@terraform_init_log = nil
103
- def terraform_init_log
104
- return @@terraform_init_log if @@terraform_init_log
105
- basename = File.basename(Tempfile.new('terraform-init').path)
106
- @@terraform_init_log = "#{Terraspace.tmp_root}/out/#{basename}.out"
119
+ #
120
+ # Log for init is in /tmp because using shell >> redirection
121
+ # It requires full path since we're running terraform within the .terraspace-cache folder
122
+ # This keeps the printed command shorter:
123
+ #
124
+ # => terraform init -get >> /tmp/terraspace/log/init/demo.log
125
+ #
126
+ def terraform_init_log(mod_name)
127
+ "#{Terraspace.tmp_root}/log/init/#{mod_name}.log"
107
128
  end
129
+ memoize :terraform_init_log
130
+ end
131
+
132
+ private
133
+ def same_file?(src, dest)
134
+ src == dest
108
135
  end
109
136
  end
110
137
  end
@@ -0,0 +1,13 @@
1
+ module Terraspace::Terraform::Cloud
2
+ class Runs < Terraspace::CLI::Base
3
+ def list
4
+ lister = Lister.new(@mod, @options)
5
+ lister.run
6
+ end
7
+
8
+ def prune
9
+ pruner = Pruner.new(@mod, @options)
10
+ pruner.run
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ class Terraspace::Terraform::Cloud::Runs
2
+ class Base
3
+ extend Memoist
4
+ include Terraspace::Util::Logging
5
+ include Terraspace::Util::Sure
6
+ include Terraspace::Terraform::Api::Client
7
+
8
+ # Api::Client requires @mod to be set
9
+ def initialize(mod, options={})
10
+ @mod, @options = mod, options
11
+ end
12
+
13
+ def runs
14
+ runs = api.runs.list
15
+ runs.select! do |item|
16
+ @options[:status].nil? ||
17
+ @options[:status].include?("all") ||
18
+ @options[:status].include?(item['attributes']['status'])
19
+ end
20
+ runs
21
+ end
22
+ memoize :runs
23
+
24
+ def build_project
25
+ Terraspace::Builder.new(@options).run
26
+
27
+ unless remote && remote['organization']
28
+ logger.info "ERROR: There was no organization found. Are you sure you configured backend.tf with it?".color(:red)
29
+ exit 1
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,37 @@
1
+ class Terraspace::Terraform::Cloud::Runs
2
+ class ItemPresenter
3
+ attr_reader :id
4
+ def initialize(raw)
5
+ @raw = raw # raw item
6
+ @id = raw['id']
7
+ @attrs = raw['attributes']
8
+ end
9
+
10
+ def method_missing(name, *args, &block)
11
+ attrs = @attrs.transform_keys { |k| k.gsub('-','_').to_sym }
12
+ if attrs.key?(name)
13
+ attrs[name]
14
+ else
15
+ super
16
+ end
17
+ end
18
+
19
+ def message
20
+ max = 25
21
+ message = @attrs['message']
22
+ if message.size >= max
23
+ message[0..max] + "..."
24
+ else
25
+ message
26
+ end
27
+ end
28
+
29
+ def created_at
30
+ pretty_time(@attrs['created-at'])
31
+ end
32
+
33
+ def pretty_time(text)
34
+ text.sub(/\..*/,'')
35
+ end
36
+ end
37
+ end