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,22 @@
1
+ require 'cli-format'
2
+
3
+ class Terraspace::Terraform::Cloud::Runs
4
+ class Lister < Base
5
+ def run
6
+ build_project
7
+ if runs.empty?
8
+ logger.info "No runs found"
9
+ return
10
+ end
11
+
12
+ presenter = CliFormat::Presenter.new(@options)
13
+ presenter.header = ["Id", "Status", "Message", "Created At"]
14
+ runs.each do |item|
15
+ p = ItemPresenter.new(item)
16
+ row = [p.id, p.status, p.message, p.created_at]
17
+ presenter.rows << row
18
+ end
19
+ presenter.show
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,109 @@
1
+ class Terraspace::Terraform::Cloud::Runs
2
+ class Pruner < Base
3
+ include Terraspace::Terraform::Api::Client
4
+
5
+ # @mod required for Api::Client
6
+ def initialize(mod, options={})
7
+ super
8
+ @queue, @kept, @needs_pruning = [], nil, false
9
+ end
10
+
11
+ def run
12
+ build_project
13
+ build_queue
14
+ are_you_sure?
15
+ process_queue
16
+ end
17
+
18
+ private
19
+ def build_queue
20
+ runs.each do |item|
21
+ next unless actionable?(item)
22
+ unless @kept
23
+ @kept = item
24
+ next
25
+ end
26
+ queue(item)
27
+ @needs_pruning = true
28
+ end
29
+ end
30
+
31
+ def are_you_sure?
32
+ unless @needs_pruning
33
+ logger.info "Nothing to prune"
34
+ return
35
+ end
36
+
37
+ keeping = item_message(@kept)
38
+ items = @queue.map { |i| item_message(i) }.join("\n")
39
+ message =<<~EOL
40
+ Will keep:
41
+
42
+ #{keeping}
43
+
44
+ Will prune:
45
+
46
+ #{items}
47
+
48
+ Are you sure?
49
+ EOL
50
+ sure?(message.chop)
51
+ end
52
+
53
+ def item_message(item)
54
+ p = ItemPresenter.new(item)
55
+ " #{p.id} #{p.status} #{p.message} #{p.created_at}"
56
+ end
57
+
58
+ def process_queue
59
+ @queue.each do |item|
60
+ process(item)
61
+ end
62
+ end
63
+
64
+ def process(item)
65
+ id = item['id']
66
+ action = discardable?(item) ? "Discarded" : "Cancelled"
67
+ p = ItemPresenter.new(item)
68
+ msg = "#{action} #{p.id} #{p.message}" # note id is named run-xxx
69
+ logger.info("NOOP: #{msg}") && return if @options[:noop]
70
+
71
+ if discardable?(item)
72
+ api.runs.discard(id)
73
+ elsif cancelable?(item)
74
+ api.runs.cancel(id)
75
+ end
76
+ logger.info msg
77
+ end
78
+
79
+ def queue(item)
80
+ if discardable?(item)
81
+ @queue << item
82
+ elsif cancelable?(item)
83
+ @queue << item
84
+ end
85
+ end
86
+
87
+ # Docs seem to be off: https://www.terraform.io/docs/cloud/api/run.html#apply-a-run
88
+ #
89
+ # This includes runs in the "pending," "needs confirmation," "policy checked," and "policy override" states.
90
+ #
91
+ # Cant really discard a "pending" status, but can discard a "planned" status.
92
+ #
93
+ def actionable?(item)
94
+ discardable?(item) || cancelable?(item)
95
+ end
96
+
97
+ def discardable?(item)
98
+ %w[planned].include?(status(item))
99
+ end
100
+
101
+ def cancelable?(item)
102
+ %w[pending].include?(status(item))
103
+ end
104
+
105
+ def status(item)
106
+ item['attributes']['status']
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,41 @@
1
+ module Terraspace::Terraform::Cloud
2
+ class Sync < Terraspace::CLI::Base
3
+ extend Memoist
4
+ include Terraspace::Terraform::Api::Client
5
+
6
+ # Note about why workspace.create is called:
7
+ #
8
+ # CLI::Init#run
9
+ # init => runs `terraform init`
10
+ # build_remote_dependencies
11
+ # sync_cloud => leads to create_workspace
12
+ #
13
+ # The `terraform init` will auto-create the TFC workspace
14
+ # If there is a .terraform folder the config.init.mode == "auto" though,
15
+ # then the workspace won't be created.
16
+ # So we check and create the workspace if necessary.
17
+ def run
18
+ # Note: workspace still gets created by `terraform init` However, variables wont be sync if returns early
19
+ return unless Terraspace.config.cloud.auto_sync || @options[:override_auto_sync]
20
+ return unless workspaces_backend?
21
+ logger.info "Syncing to Terraform Cloud: #{@mod.name} => #{workspace_name}"
22
+ @api = Terraspace::Terraform::Api.new(@mod, remote)
23
+ workspace.create_or_update
24
+ workspace.set_working_dir
25
+ workspace.set_env_vars
26
+ end
27
+
28
+ def workspace
29
+ @api.workspace
30
+ end
31
+
32
+ def workspaces_backend?
33
+ remote && remote['workspaces']
34
+ end
35
+
36
+ # already memoized in Api::Client
37
+ def backend
38
+ Terraspace::Compiler::Backend::Parser.new(@mod).result
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,52 @@
1
+ module Terraspace::Terraform::Cloud
2
+ class Syncer < Terraspace::CLI::Base
3
+ extend Memoist
4
+ include Terraspace::Compiler::DirsConcern
5
+ include Terraspace::Util::Sure
6
+
7
+ def run
8
+ are_you_sure?
9
+ mods.each do |mod|
10
+ run_sync(mod)
11
+ end
12
+ end
13
+
14
+ def mods
15
+ mod = @options[:mod]
16
+ mod ? [mod] : stack_names
17
+ end
18
+
19
+ def run_sync(mod)
20
+ sync(mod).run
21
+ end
22
+
23
+ def sync(mod)
24
+ Sync.new(@options.merge(mod: mod))
25
+ end
26
+ memoize :sync
27
+
28
+ def are_you_sure?
29
+ message =<<~EOL
30
+ About to sync these project stacks with Terraform Cloud workspaces:
31
+
32
+ Stack => Workspace
33
+ EOL
34
+
35
+ mods.each do |mod|
36
+ sync = sync(mod)
37
+ message << " #{mod} => #{sync.workspace_name}\n"
38
+ end
39
+ message << <<~EOL
40
+
41
+ A sync does the following for each workspace:
42
+
43
+ 1. Create or update workspace, including the VCS settings.
44
+ 2. Set the working dir.
45
+ 3. Set env and terraform variables.
46
+
47
+ Are you sure?
48
+ EOL
49
+ sure?(message.chop)
50
+ end
51
+ end
52
+ end
@@ -1,8 +1,9 @@
1
- class Terraspace::Terraform::Cloud
1
+ module Terraspace::Terraform::Cloud
2
2
  class Workspace < Terraspace::CLI::Base
3
3
  extend Memoist
4
4
  include Terraspace::Util::Logging
5
5
  include Terraspace::Terraform::Api::Client
6
+ include Terraspace::Terraform::Api::Http::Concern
6
7
 
7
8
  # List will not have @mod set.
8
9
  def list
@@ -32,10 +33,15 @@ class Terraspace::Terraform::Cloud
32
33
  Terraspace::CLI::Init.new(@options.merge(calling_command: "cloud-setup")).run
33
34
  end
34
35
 
36
+ def create
37
+ build
38
+ return unless api
39
+ api.workspace.create
40
+ end
41
+
35
42
  def destroy
36
43
  build
37
- return unless backend.dig('remote','workspaces') # in case called by terraspace down demo -y --destroy-workspace with a non-remote backend
38
- api = Terraspace::Terraform::Api.new(@mod, remote)
44
+ return unless api
39
45
  workspace = api.workspace(exit_on_fail: false)
40
46
  unless workspace
41
47
  logger.info "Workspace #{workspace_name} not found for #{@mod.type}: #{@mod.name}"
@@ -43,25 +49,8 @@ class Terraspace::Terraform::Cloud
43
49
  end
44
50
  sure?
45
51
  logger.info "Destroying workspace #{workspace_name}"
46
- api.destroy_workspace
47
- end
48
-
49
- def build
50
- Terraspace::Builder.new(@options).run
51
- end
52
-
53
- def workspace_name
54
- remote['workspaces']['name']
55
- end
56
-
57
- def remote
58
- backend["remote"]
59
- end
60
-
61
- def backend
62
- Terraspace::Compiler::Backend::Parser.new(@mod).result
52
+ api.workspace.destroy
63
53
  end
64
- memoize :backend
65
54
 
66
55
  def sure?
67
56
  message = <<~EOL.chop + " " # chop to remove newline
@@ -34,7 +34,7 @@ module Terraspace::Terraform::Hooks
34
34
  exit_on_fail = exit_on_fail.nil? ? true : exit_on_fail
35
35
 
36
36
  logger.info "Running #{type} hook"
37
- sh(execute, exit_on_fail: exit_on_fail)
37
+ Terraspace::Shell.new(@mod, execute, exit_on_fail: exit_on_fail).run
38
38
  end
39
39
  end
40
40
  end
@@ -0,0 +1,143 @@
1
+ module Terraspace::Terraform::RemoteState
2
+ class Fetcher
3
+ extend Memoist
4
+ include Terraspace::Compiler::CommandsConcern
5
+ include Terraspace::Util::Logging
6
+
7
+ def initialize(parent, identifier, options={})
8
+ @parent, @identifier, @options = parent, identifier, options
9
+ child_name, @output_key = identifier.split('.')
10
+ @child = Terraspace::Mod.new(child_name)
11
+ end
12
+
13
+ def run
14
+ validate! # check child stack exists
15
+ pull
16
+ load
17
+ end
18
+
19
+ def output
20
+ run
21
+ if pull_success?
22
+ value = output_value
23
+ error = output_error(:key_not_found) unless @outputs.key?(@output_key)
24
+ OutputProxy.new(value, @options.merge(error: error))
25
+ else
26
+ @error_type ||= :state_not_found # could be set to :bucket_not_found by bucket_not_found_error
27
+ error = output_error(@error_type)
28
+ OutputProxy.new(nil, @options.merge(error: error))
29
+ end
30
+ end
31
+
32
+ def output_value
33
+ return unless @outputs.key?(@output_key)
34
+ result = @outputs.dig(@output_key)
35
+ result.dig("value") if result
36
+ end
37
+
38
+ def output_error(type)
39
+ msg = case type
40
+ when :key_not_found
41
+ "Output #{@output_key} was not found for the #{@parent.name} tfvars file. Either #{@child.name} stack has not been deployed yet or it does not have this output: #{@output_key}"
42
+ when :state_not_found
43
+ "Output #{@output_key} could not be looked up for the #{@parent.name} tfvars file. #{@child.name} stack needs to be deployed"
44
+ when :bucket_not_found
45
+ "The bucket for the backend could not be found"
46
+ end
47
+ msg = "(#{msg})"
48
+ log_message(msg)
49
+ msg
50
+ end
51
+
52
+ @@pull_successes = {}
53
+ @@download_shown = false
54
+ def pull
55
+ return if @@pull_successes[cache_key]
56
+ logger.info "Downloading tfstate files for dependencies defined in tfvars..." unless @@download_shown || @options[:quiet]
57
+ @@download_shown = true
58
+ logger.debug "Downloading tfstate for stack: #{@child.name}"
59
+
60
+ success = init # init not yet run. only run .init directly, not .run. init can completely error and early exit.
61
+ return unless success
62
+
63
+ FileUtils.mkdir_p(File.dirname(state_path))
64
+ command = "cd #{@child.cache_dir} && terraform state pull > #{state_path}"
65
+ logger.debug "=> #{command}"
66
+ success = system(command)
67
+ # Can error if using a old terraform version and the statefile was created with a newer version of terraform
68
+ # IE: Failed to refresh state: state snapshot was created by Terraform v0.13.2, which is newer than current v0.12.29;
69
+ # upgrade to Terraform v0.13.2 or greater to work with this state
70
+ unless success
71
+ logger.info "Error running: #{command}".color(:red)
72
+ logger.info "Please fix the error before continuing"
73
+ end
74
+ @@pull_successes[cache_key] = success
75
+ end
76
+
77
+ def init
78
+ Terraspace::CLI::Init.new(mod: @child.name, calling_command: "apply", quiet: true, suppress_error_color: true).init
79
+ true
80
+ rescue Terraspace::BucketNotFoundError # from Terraspace::Shell
81
+ bucket_not_found_error
82
+ false
83
+ end
84
+
85
+ # mimic pull error
86
+ def bucket_not_found_error
87
+ @@pull_successes[cache_key] = false
88
+ @error_type = :bucket_not_found
89
+ end
90
+
91
+ def load
92
+ return self unless pull_success?
93
+
94
+ # use or set cache
95
+ if @@cache[cache_key]
96
+ @outputs = @@cache[cache_key]
97
+ else
98
+ @outputs = @@cache[cache_key] = read_statefile_outputs
99
+ end
100
+
101
+ self
102
+ end
103
+ memoize :load
104
+
105
+ def cache_key
106
+ @child.name
107
+ end
108
+
109
+ def read_statefile_outputs
110
+ data = JSON.load(IO.read(state_path))
111
+ data ? data['outputs'] : {}
112
+ end
113
+
114
+ def pull_success?
115
+ @@pull_successes[cache_key]
116
+ end
117
+
118
+ def state_path
119
+ "#{Terraspace.tmp_root}/remote_state/#{@child.build_dir}/state.json"
120
+ end
121
+
122
+ # Note we already validate mod exist at the terraform_output helper. This is just in case that logic changes.
123
+ def validate!
124
+ return if @child.exist?
125
+ logger.error "ERROR: stack #{@child.name} not found".color(:red)
126
+ exit 1
127
+ end
128
+
129
+ # Using debug level because all the tfvar files always get evaluated.
130
+ # So dont want these messages to show up and be noisy unless debugging.
131
+ def log_message(msg)
132
+ logger.debug "DEBUG: #{msg}".color(:yellow)
133
+ end
134
+
135
+ cattr_accessor :cache, default: {}
136
+ class << self
137
+ def flush!
138
+ @@pull_successes = {}
139
+ @@cache = {}
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,39 @@
1
+ module Terraspace::Terraform::RemoteState::Marker
2
+ class Output
3
+ include Terraspace::Util::Logging
4
+
5
+ def initialize(mod, identifier, options={})
6
+ @mod, @identifier, @options = mod, identifier, options
7
+ @parent_name = @mod.name
8
+ @child_name, @output_key = @identifier.split('.')
9
+ end
10
+
11
+ def build
12
+ if valid?
13
+ Terraspace::Dependency::Registry.register(@parent_name, @child_name)
14
+ else
15
+ warning
16
+ end
17
+ # MARKER for debugging. Only appears on 1st pass. Will not see unless changing Terraspace code for debugging.
18
+ marker = "MARKER:terraform_output('#{@identifier}')"
19
+ Terraspace::Terraform::RemoteState::OutputProxy.new(marker, @options)
20
+ end
21
+
22
+ def valid?
23
+ self.class.stack_names.include?(@child_name)
24
+ end
25
+
26
+ def warning
27
+ logger.warn "WARN: The #{@child_name} stack does not exist".color(:yellow)
28
+ caller_line = caller.find { |l| l.include?('.tfvars') }
29
+ source_code = PrettyTracer.new(caller_line).source_code
30
+ logger.info source_code
31
+ end
32
+
33
+ class << self
34
+ extend Memoist
35
+ # Marker::Output uses DirsConcern stack_names to check if stacks are valid
36
+ include Terraspace::Compiler::DirsConcern
37
+ end
38
+ end
39
+ end