terraspace 0.2.3 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +14 -1
- data/lib/templates/base/git_hook/hook.sh +1 -1
- data/lib/templates/base/project/.gitignore +1 -0
- data/lib/templates/base/project/README.md +17 -0
- data/lib/terraspace.rb +5 -0
- data/lib/terraspace/all/base.rb +8 -0
- data/lib/terraspace/all/grapher.rb +129 -0
- data/lib/terraspace/all/preview.rb +43 -0
- data/lib/terraspace/all/runner.rb +169 -0
- data/lib/terraspace/all/summary.rb +99 -0
- data/lib/terraspace/app.rb +31 -9
- data/lib/terraspace/booter.rb +9 -0
- data/lib/terraspace/builder.rb +59 -22
- data/lib/terraspace/cli.rb +39 -12
- data/lib/terraspace/cli/all.rb +63 -0
- data/lib/terraspace/cli/build/placeholder.rb +2 -5
- data/lib/terraspace/cli/bundle.rb +1 -1
- data/lib/terraspace/cli/check_setup.rb +17 -5
- data/lib/terraspace/cli/cloud.rb +18 -2
- data/lib/terraspace/cli/cloud/runs.rb +24 -0
- data/lib/terraspace/cli/commander.rb +1 -8
- data/lib/terraspace/cli/down.rb +20 -0
- data/lib/terraspace/cli/help/cloud/runs/list.md +36 -0
- data/lib/terraspace/cli/help/cloud/runs/prune.md +25 -0
- data/lib/terraspace/cli/help/cloud/sync.md +19 -0
- data/lib/terraspace/cli/help/log.md +46 -0
- data/lib/terraspace/cli/init.rb +35 -7
- data/lib/terraspace/cli/list.rb +14 -1
- data/lib/terraspace/cli/log.rb +112 -0
- data/lib/terraspace/cli/log/concern.rb +24 -0
- data/lib/terraspace/cli/logs.rb +15 -0
- data/lib/terraspace/cli/logs/tasks.rb +32 -0
- data/lib/terraspace/cli/new/git_hook.rb +1 -1
- data/lib/terraspace/cli/tfc_concern.rb +14 -0
- data/lib/terraspace/cli/up.rb +32 -0
- data/lib/terraspace/compiler/backend.rb +10 -0
- data/lib/terraspace/compiler/builder.rb +5 -4
- data/lib/terraspace/compiler/cleaner.rb +1 -1
- data/lib/terraspace/compiler/cleaner/backend_change.rb +21 -7
- data/lib/terraspace/compiler/commands_concern.rb +18 -0
- data/lib/terraspace/compiler/dirs_concern.rb +47 -0
- data/lib/terraspace/compiler/dsl/syntax/helpers/common.rb +26 -1
- data/lib/terraspace/core.rb +11 -2
- data/lib/terraspace/dependency/graph.rb +139 -0
- data/lib/terraspace/dependency/node.rb +38 -0
- data/lib/terraspace/dependency/registry.rb +11 -0
- data/lib/terraspace/logger.rb +6 -18
- data/lib/terraspace/logger/formatter.rb +13 -0
- data/lib/terraspace/mod.rb +7 -1
- data/lib/terraspace/seeder/where.rb +6 -2
- data/lib/terraspace/shell.rb +97 -0
- data/lib/terraspace/terraform/api.rb +7 -45
- data/lib/terraspace/terraform/api/base.rb +7 -0
- data/lib/terraspace/terraform/api/client.rb +23 -3
- data/lib/terraspace/terraform/api/http.rb +14 -34
- data/lib/terraspace/terraform/api/http/concern.rb +10 -0
- data/lib/terraspace/terraform/api/runs.rb +28 -0
- data/lib/terraspace/terraform/api/token.rb +65 -0
- data/lib/terraspace/terraform/api/var.rb +20 -6
- data/lib/terraspace/terraform/api/vars.rb +2 -1
- data/lib/terraspace/terraform/api/workspace.rb +98 -0
- data/lib/terraspace/terraform/args/default.rb +48 -21
- data/lib/terraspace/terraform/cloud/runs.rb +13 -0
- data/lib/terraspace/terraform/cloud/runs/base.rb +33 -0
- data/lib/terraspace/terraform/cloud/runs/item_presenter.rb +37 -0
- data/lib/terraspace/terraform/cloud/runs/lister.rb +22 -0
- data/lib/terraspace/terraform/cloud/runs/pruner.rb +109 -0
- data/lib/terraspace/terraform/cloud/sync.rb +41 -0
- data/lib/terraspace/terraform/cloud/syncer.rb +52 -0
- data/lib/terraspace/terraform/cloud/workspace.rb +10 -21
- data/lib/terraspace/terraform/hooks/builder.rb +1 -1
- data/lib/terraspace/terraform/remote_state/fetcher.rb +143 -0
- data/lib/terraspace/terraform/remote_state/marker/output.rb +39 -0
- data/lib/terraspace/terraform/remote_state/marker/pretty_tracer.rb +37 -0
- data/lib/terraspace/terraform/remote_state/output_proxy.rb +29 -0
- data/lib/terraspace/terraform/runner.rb +24 -14
- data/lib/terraspace/util.rb +1 -5
- data/lib/terraspace/util/pretty.rb +18 -0
- data/lib/terraspace/version.rb +1 -1
- data/spec/fixtures/fetcher/c1.json +37 -0
- data/spec/fixtures/parser/cache_dirs/all/01-test.auto.tfvars +5 -0
- data/spec/fixtures/parser/cache_dirs/depends_on/01-test.auto.tfvars +2 -0
- data/spec/fixtures/parser/cache_dirs/output/01-test.auto.tfvars +2 -0
- data/spec/fixtures/summary/down.log +12 -0
- data/spec/fixtures/summary/output.log +5 -0
- data/spec/fixtures/summary/plan/error.log +20 -0
- data/spec/fixtures/summary/plan/success.log +17 -0
- data/spec/fixtures/summary/show.log +22 -0
- data/spec/fixtures/summary/up/error.log +13 -0
- data/spec/fixtures/summary/up/success.log +63 -0
- data/spec/fixtures/summary/validate/error.log +13 -0
- data/spec/fixtures/summary/validate/success.log +5 -0
- data/spec/terraspace/all/grapher_spec.rb +38 -0
- data/spec/terraspace/all/runner_spec.rb +48 -0
- data/spec/terraspace/all/summary_spec.rb +93 -0
- data/spec/terraspace/dependency/graph_spec.rb +162 -0
- data/spec/terraspace/seeder_spec.rb +0 -1
- data/spec/terraspace/terraform/remote_state/fetcher_spec.rb +52 -0
- data/terraspace.gemspec +5 -1
- metadata +138 -5
- data/lib/terraspace/terraform/cloud.rb +0 -25
- 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
|
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
|
-
|
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
|
-
|
27
|
+
vars.overwrite_sensitive
|
28
28
|
else
|
29
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
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
|
67
|
-
|
68
|
-
FileUtils.mkdir_p(File.dirname(
|
69
|
-
args << "
|
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
|
95
|
+
def show_args
|
81
96
|
args = []
|
82
|
-
args << "-json" if @options[:
|
83
|
-
args << "
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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,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
|