terraspace 0.5.9 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE.md +2 -2
- data/.github/ISSUE_TEMPLATE/bug_report.md +4 -3
- data/.github/ISSUE_TEMPLATE/question.md +2 -2
- data/CHANGELOG.md +29 -7
- data/README.md +2 -0
- data/lib/terraspace.rb +1 -0
- data/lib/terraspace/app.rb +36 -16
- data/lib/terraspace/app/inits.rb +13 -0
- data/lib/terraspace/booter.rb +1 -0
- data/lib/terraspace/cli.rb +13 -6
- data/lib/terraspace/cli/build/placeholder.rb +6 -1
- data/lib/terraspace/cli/commander.rb +1 -1
- data/lib/terraspace/cli/down.rb +1 -1
- data/lib/terraspace/cli/help/{cloud → tfc}/destroy.md +1 -1
- data/lib/terraspace/cli/help/{cloud → tfc}/list.md +1 -1
- data/lib/terraspace/cli/help/{cloud → tfc}/runs/list.md +3 -3
- data/lib/terraspace/cli/help/{cloud → tfc}/runs/prune.md +3 -3
- data/lib/terraspace/cli/help/{cloud → tfc}/sync.md +3 -3
- data/lib/terraspace/cli/init.rb +21 -8
- data/lib/terraspace/cli/state.rb +10 -0
- data/lib/terraspace/cli/{cloud.rb → tfc.rb} +3 -3
- data/lib/terraspace/cli/{cloud → tfc}/runs.rb +4 -4
- data/lib/terraspace/cli/up.rb +5 -3
- data/lib/terraspace/compiler/builder.rb +2 -0
- data/lib/terraspace/compiler/dirs_concern.rb +11 -1
- data/lib/terraspace/compiler/select.rb +28 -0
- data/lib/terraspace/compiler/strategy/tfvar.rb +16 -4
- data/lib/terraspace/compiler/strategy/tfvar/layer.rb +75 -52
- data/lib/terraspace/layering.rb +24 -0
- data/lib/terraspace/logger.rb +8 -1
- data/lib/terraspace/mod.rb +18 -3
- data/lib/terraspace/plugin/expander/friendly.rb +10 -0
- data/lib/terraspace/plugin/expander/interface.rb +6 -1
- data/lib/terraspace/plugin/summary/interface.rb +1 -1
- data/lib/terraspace/shell.rb +16 -1
- data/lib/terraspace/shell/error.rb +1 -1
- data/lib/terraspace/terraform/api/runs.rb +13 -2
- data/lib/terraspace/terraform/api/token.rb +2 -2
- data/lib/terraspace/terraform/api/var.rb +1 -1
- data/lib/terraspace/terraform/api/vars.rb +1 -1
- data/lib/terraspace/terraform/api/vars/base.rb +2 -0
- data/lib/terraspace/terraform/api/vars/json.rb +13 -1
- data/lib/terraspace/terraform/api/workspace.rb +10 -3
- data/lib/terraspace/terraform/args/default.rb +23 -13
- data/lib/terraspace/terraform/ihooks/after/plan.rb +17 -0
- data/lib/terraspace/terraform/ihooks/base.rb +8 -0
- data/lib/terraspace/terraform/ihooks/before/plan.rb +14 -0
- data/lib/terraspace/terraform/remote_state/fetcher.rb +1 -1
- data/lib/terraspace/terraform/runner.rb +12 -0
- data/lib/terraspace/terraform/{cloud → tfc}/runs.rb +1 -1
- data/lib/terraspace/terraform/{cloud → tfc}/runs/base.rb +1 -1
- data/lib/terraspace/terraform/{cloud → tfc}/runs/item_presenter.rb +1 -1
- data/lib/terraspace/terraform/{cloud → tfc}/runs/lister.rb +1 -1
- data/lib/terraspace/terraform/{cloud → tfc}/runs/pruner.rb +1 -1
- data/lib/terraspace/terraform/{cloud → tfc}/sync.rb +2 -2
- data/lib/terraspace/terraform/{cloud → tfc}/syncer.rb +1 -1
- data/lib/terraspace/terraform/{cloud → tfc}/workspace.rb +2 -3
- data/lib/terraspace/util/pretty.rb +2 -1
- data/lib/terraspace/version.rb +1 -1
- metadata +26 -18
@@ -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.expansion
|
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
|
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}"), '')
|
data/lib/terraspace/shell.rb
CHANGED
@@ -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
|
-
|
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
|
@@ -13,7 +13,7 @@ class Terraspace::Shell
|
|
13
13
|
if reinit_required?
|
14
14
|
Terraspace::InitRequiredError.new(@lines)
|
15
15
|
elsif bucket_not_found?
|
16
|
-
Terraspace::
|
16
|
+
Terraspace::BucketNotFoundError.new(@lines)
|
17
17
|
elsif shared_cache_error?
|
18
18
|
Terraspace::SharedCacheError.new(@lines)
|
19
19
|
end
|
@@ -8,8 +8,19 @@ class Terraspace::Terraform::Api
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def list
|
11
|
-
|
12
|
-
|
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['
|
54
|
+
ENV['TFC_HOST'] || Terraspace.config.tfc.hostname || 'app.terraform.io'
|
55
55
|
end
|
56
56
|
|
57
57
|
def hostname_configured?
|
58
|
-
!!Terraspace.config.
|
58
|
+
!!Terraspace.config.tfc.hostname
|
59
59
|
end
|
60
60
|
|
61
61
|
def self.get
|
@@ -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/
|
36
|
+
Dir.glob("#{Terraspace.root}/config/terraform/tfc/vars.{rb,json}").first
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
@@ -4,11 +4,23 @@ class Terraspace::Terraform::Api::Vars
|
|
4
4
|
context = Terraspace::Compiler::Erb::Context.new(@mod)
|
5
5
|
result = RenderMePretty.result(@vars_path, context: context)
|
6
6
|
|
7
|
-
data =
|
7
|
+
data = json_load(result)
|
8
8
|
items = data.select do |item|
|
9
9
|
item['data']['type'] == 'vars'
|
10
10
|
end
|
11
11
|
items.map { |i| i['data']['attributes'] }
|
12
12
|
end
|
13
|
+
|
14
|
+
def json_load(result)
|
15
|
+
JSON.load(result)
|
16
|
+
rescue JSON::ParserError => e
|
17
|
+
# TODO: show exact line with error
|
18
|
+
logger.info("ERROR in json: #{e.class}: #{e.message}")
|
19
|
+
path = "/tmp/terraspace/debug/vars.json"
|
20
|
+
logger.info("Result also written to #{path} for inspection")
|
21
|
+
FileUtils.mkdir_p(File.dirname(path))
|
22
|
+
IO.write(path, result)
|
23
|
+
exit 1
|
24
|
+
end
|
13
25
|
end
|
14
26
|
end
|
@@ -24,7 +24,7 @@ class Terraspace::Terraform::Api
|
|
24
24
|
|
25
25
|
def working_directory
|
26
26
|
cache_dir = @mod.cache_dir.sub("#{Terraspace.root}/", '')
|
27
|
-
prefix = Terraspace.config.
|
27
|
+
prefix = Terraspace.config.tfc.working_dir_prefix # prepended to TFC Working Directory
|
28
28
|
prefix ? "#{prefix}/#{cache_dir}" : cache_dir
|
29
29
|
end
|
30
30
|
|
@@ -38,7 +38,8 @@ class Terraspace::Terraform::Api
|
|
38
38
|
#
|
39
39
|
# terraspace up demo --no-init
|
40
40
|
#
|
41
|
-
|
41
|
+
exit_on_fail = options[:exit_on_fail].nil? ? true : options[:exit_on_fail]
|
42
|
+
if exit_on_fail && not_found_error?(payload)
|
42
43
|
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
44
|
exit 1
|
44
45
|
end
|
@@ -46,6 +47,12 @@ class Terraspace::Terraform::Api
|
|
46
47
|
end
|
47
48
|
memoize :details
|
48
49
|
|
50
|
+
def not_found_error?(payload)
|
51
|
+
return true unless payload
|
52
|
+
return false unless payload.key?('errors')
|
53
|
+
payload['errors'][0]['status'] == '404'
|
54
|
+
end
|
55
|
+
|
49
56
|
def destroy
|
50
57
|
# response payload from delete operation is nil
|
51
58
|
http.delete("/organizations/#{@organization}/workspaces/#{@name}")
|
@@ -74,7 +81,7 @@ class Terraspace::Terraform::Api
|
|
74
81
|
|
75
82
|
def attributes
|
76
83
|
attrs = { name: @name }
|
77
|
-
config = Terraspace.config.
|
84
|
+
config = Terraspace.config.tfc.workspace.attrs
|
78
85
|
attrs.merge!(config)
|
79
86
|
# Default: run on all changes since app/modules can affect app/stacks
|
80
87
|
if config['vcs-repo'] && config['file-triggers-enabled'].nil?
|
@@ -23,7 +23,10 @@ module Terraspace::Terraform::Args
|
|
23
23
|
args = auto_approve_arg
|
24
24
|
var_files = @options[:var_files]
|
25
25
|
if var_files
|
26
|
-
|
26
|
+
var_files.each do |file|
|
27
|
+
copy_to_cache(plan)
|
28
|
+
end
|
29
|
+
args << var_files.map { |f| "-var-file #{f}" }.join(' ')
|
27
30
|
end
|
28
31
|
|
29
32
|
args << input_option
|
@@ -31,15 +34,8 @@ module Terraspace::Terraform::Args
|
|
31
34
|
# must be at the end
|
32
35
|
plan = @options[:plan]
|
33
36
|
if plan
|
34
|
-
|
35
|
-
|
36
|
-
dest = src
|
37
|
-
else
|
38
|
-
src = "#{Dir.pwd}/#{plan}"
|
39
|
-
dest = "#{@mod.cache_dir}/#{File.basename(src)}"
|
40
|
-
end
|
41
|
-
FileUtils.cp(src, dest) unless same_file?(src, dest)
|
42
|
-
args << " #{dest}"
|
37
|
+
copy_to_cache(plan)
|
38
|
+
args << " #{plan}"
|
43
39
|
end
|
44
40
|
args
|
45
41
|
end
|
@@ -79,19 +75,24 @@ module Terraspace::Terraform::Args
|
|
79
75
|
args << input_option
|
80
76
|
args << "-destroy" if @options[:destroy]
|
81
77
|
args << "-out #{expanded_out}" if @options[:out]
|
78
|
+
# Note: based on the @options[:out] will run an internal hook to copy plan
|
79
|
+
# file back up to the root project folder for use. Think this is convenient and expected behavior.
|
82
80
|
args
|
83
81
|
end
|
84
82
|
|
85
83
|
def show_args
|
86
84
|
args = []
|
87
85
|
args << " -json" if @options[:json]
|
88
|
-
|
86
|
+
plan = @options[:plan]
|
87
|
+
if plan
|
88
|
+
copy_to_cache(@options[:plan])
|
89
|
+
args << " #{@options[:plan]}" # terraform show /path/to/plan
|
90
|
+
end
|
89
91
|
args
|
90
92
|
end
|
91
93
|
|
92
94
|
def expanded_out
|
93
|
-
|
94
|
-
out.starts_with?('/') ? out : "#{Dir.pwd}/#{out}"
|
95
|
+
@options[:out]
|
95
96
|
end
|
96
97
|
|
97
98
|
def destroy_args
|
@@ -123,5 +124,14 @@ module Terraspace::Terraform::Args
|
|
123
124
|
def same_file?(src, dest)
|
124
125
|
src == dest
|
125
126
|
end
|
127
|
+
|
128
|
+
def copy_to_cache(file)
|
129
|
+
return if file =~ %r{^/} # not need to copy absolute path
|
130
|
+
name = file.sub("#{Terraspace.root}/",'')
|
131
|
+
src = name
|
132
|
+
dest = "#{@mod.cache_dir}/#{name}"
|
133
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
134
|
+
FileUtils.cp(src, dest) unless same_file?(src, dest)
|
135
|
+
end
|
126
136
|
end
|
127
137
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Terraspace::Terraform::Ihooks::After
|
2
|
+
class Plan < Terraspace::Terraform::Ihooks::Base
|
3
|
+
def run
|
4
|
+
return if !@options[:out] || @options[:copy_to_root] == false
|
5
|
+
copy_to_root(@options[:out])
|
6
|
+
end
|
7
|
+
|
8
|
+
def copy_to_root(file)
|
9
|
+
return if file =~ %r{^/} # not need to copy absolute path
|
10
|
+
name = file.sub("#{Terraspace.root}/",'')
|
11
|
+
src = "#{@mod.cache_dir}/#{name}"
|
12
|
+
dest = name
|
13
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
14
|
+
FileUtils.cp(src, dest)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Terraspace::Terraform::Ihooks::Before
|
2
|
+
class Plan < Terraspace::Terraform::Ihooks::Base
|
3
|
+
def run
|
4
|
+
out = @options[:out]
|
5
|
+
return unless out
|
6
|
+
return if out =~ %r{^/} # not need to create parent dir for copy with absolute path
|
7
|
+
|
8
|
+
out = @options[:out]
|
9
|
+
name = out.sub("#{Terraspace.root}/",'')
|
10
|
+
dest = "#{@mod.cache_dir}/#{name}"
|
11
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -84,7 +84,7 @@ module Terraspace::Terraform::RemoteState
|
|
84
84
|
end
|
85
85
|
|
86
86
|
def init
|
87
|
-
Terraspace::CLI::Init.new(mod: @child.name,
|
87
|
+
Terraspace::CLI::Init.new(mod: @child.name, quiet: true, suppress_error_color: true).init
|
88
88
|
true
|
89
89
|
rescue Terraspace::BucketNotFoundError # from Terraspace::Shell
|
90
90
|
bucket_not_found_error
|
@@ -22,7 +22,9 @@ module Terraspace::Terraform
|
|
22
22
|
params = args.flatten.join(' ')
|
23
23
|
command = "terraform #{name} #{params}".squish
|
24
24
|
run_hooks("terraform.rb", name) do
|
25
|
+
run_internal_hook(:before, name)
|
25
26
|
Terraspace::Shell.new(@mod, command, @options.merge(env: custom.env_vars)).run
|
27
|
+
run_internal_hook(:after, name)
|
26
28
|
end
|
27
29
|
rescue Terraspace::SharedCacheError, Terraspace::InitRequiredError
|
28
30
|
@retryer ||= Retryer.new(@mod, @options, name, $!)
|
@@ -34,6 +36,16 @@ module Terraspace::Terraform
|
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
39
|
+
def run_internal_hook(type, name)
|
40
|
+
begin
|
41
|
+
klass = "Terraspace::Terraform::Ihooks::#{type.to_s.classify}::#{name.classify}".constantize
|
42
|
+
rescue NameError
|
43
|
+
return
|
44
|
+
end
|
45
|
+
ihook = klass.new(name, @options)
|
46
|
+
ihook.run
|
47
|
+
end
|
48
|
+
|
37
49
|
@@current_dir_message_shown = false
|
38
50
|
def current_dir_message
|
39
51
|
return if @@current_dir_message_shown
|
@@ -1,4 +1,4 @@
|
|
1
|
-
module Terraspace::Terraform::
|
1
|
+
module Terraspace::Terraform::Tfc
|
2
2
|
class Sync < Terraspace::CLI::Base
|
3
3
|
extend Memoist
|
4
4
|
include Terraspace::Terraform::Api::Client
|
@@ -16,7 +16,7 @@ module Terraspace::Terraform::Cloud
|
|
16
16
|
# So we check and create the workspace if necessary.
|
17
17
|
def run
|
18
18
|
# Note: workspace still gets created by `terraform init` However, variables wont be sync if returns early
|
19
|
-
return unless Terraspace.config.
|
19
|
+
return unless Terraspace.config.tfc.auto_sync || @options[:override_auto_sync]
|
20
20
|
return unless workspaces_backend?
|
21
21
|
logger.info "Syncing to Terraform Cloud: #{@mod.name} => #{workspace_name}"
|
22
22
|
@api = Terraspace::Terraform::Api.new(@mod, remote)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
module Terraspace::Terraform::
|
1
|
+
module Terraspace::Terraform::Tfc
|
2
2
|
class Workspace < Terraspace::CLI::Base
|
3
3
|
extend Memoist
|
4
4
|
include Terraspace::Util::Logging
|
@@ -21,7 +21,7 @@ module Terraspace::Terraform::Cloud
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def init
|
24
|
-
Terraspace::CLI::Init.new(@options
|
24
|
+
Terraspace::CLI::Init.new(@options).run
|
25
25
|
end
|
26
26
|
|
27
27
|
def create
|
@@ -72,4 +72,3 @@ module Terraspace::Terraform::Cloud
|
|
72
72
|
end
|
73
73
|
end
|
74
74
|
end
|
75
|
-
|