terraspace 0.1.1 → 0.2.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 +35 -2
- data/README.md +20 -12
- data/lib/templates/base/git_hook/hook.sh +5 -0
- data/lib/templates/base/project/.gitignore +0 -1
- data/lib/templates/base/shim/terraspace +7 -0
- data/lib/templates/hcl/project/config/terraform/backend.tf.tt +3 -3
- data/lib/templates/plugin/lib/templates/hcl/project/config/terraform/backend.tf.tt +2 -2
- data/lib/terraspace/app.rb +8 -0
- data/lib/terraspace/builder.rb +6 -45
- data/lib/terraspace/cli.rb +34 -18
- data/lib/terraspace/cli/build/placeholder.rb +40 -0
- data/lib/terraspace/cli/cloud.rb +24 -0
- data/lib/terraspace/cli/commander.rb +8 -1
- data/lib/terraspace/cli/init.rb +67 -0
- data/lib/terraspace/cli/list.rb +13 -0
- data/lib/terraspace/cli/new.rb +8 -0
- data/lib/terraspace/cli/new/git_hook.rb +33 -0
- data/lib/terraspace/cli/new/shim.rb +58 -0
- data/lib/terraspace/cli/summary.rb +9 -12
- data/lib/terraspace/compiler/backend.rb +9 -37
- data/lib/terraspace/compiler/backend/parser.rb +42 -0
- data/lib/terraspace/compiler/builder.rb +6 -2
- data/lib/terraspace/compiler/cleaner.rb +19 -2
- data/lib/terraspace/compiler/cleaner/backend_change.rb +1 -1
- data/lib/terraspace/compiler/dsl/syntax/mod.rb +1 -0
- data/lib/terraspace/compiler/dsl/syntax/mod/backend.rb +16 -3
- data/lib/terraspace/compiler/expander.rb +28 -1
- data/lib/terraspace/compiler/writer.rb +1 -1
- data/lib/terraspace/core.rb +7 -1
- data/lib/terraspace/mod.rb +37 -12
- data/lib/terraspace/mod/remote.rb +1 -1
- data/lib/terraspace/plugin/expander/interface.rb +48 -5
- data/lib/terraspace/plugin/infer_provider.rb +15 -0
- data/lib/terraspace/plugin/layer/interface.rb +5 -0
- data/lib/terraspace/plugin/summary/interface.rb +1 -0
- data/lib/terraspace/seeder.rb +4 -4
- data/lib/terraspace/terraform/api.rb +58 -0
- data/lib/terraspace/terraform/api/client.rb +10 -0
- data/lib/terraspace/terraform/api/http.rb +106 -0
- data/lib/terraspace/terraform/api/var.rb +72 -0
- data/lib/terraspace/terraform/api/vars.rb +38 -0
- data/lib/terraspace/terraform/api/vars/base.rb +7 -0
- data/lib/terraspace/terraform/api/vars/json.rb +14 -0
- data/lib/terraspace/terraform/api/vars/rb.rb +21 -0
- data/lib/terraspace/terraform/args/custom.rb +1 -1
- data/lib/terraspace/terraform/args/default.rb +16 -2
- data/lib/terraspace/terraform/cloud.rb +25 -0
- data/lib/terraspace/terraform/cloud/workspace.rb +95 -0
- data/lib/terraspace/terraform/runner.rb +1 -1
- data/lib/terraspace/util/sh.rb +1 -1
- data/lib/terraspace/version.rb +1 -1
- data/spec/fixtures/{cache_build_dir → cache_dir}/variables.tf +0 -0
- data/spec/fixtures/projects/hcl/aws/config/backend.tf +1 -1
- data/spec/fixtures/projects/hcl/google/config/backend.tf +1 -1
- data/spec/terraspace/seeder_spec.rb +1 -1
- data/spec/terraspace/terraform/hooks/builder_spec.rb +1 -1
- data/terraspace.gemspec +5 -4
- metadata +47 -13
- data/lib/terraspace/cli/build.rb +0 -7
@@ -0,0 +1,24 @@
|
|
1
|
+
class Terraspace::CLI
|
2
|
+
class Cloud < Terraspace::Command
|
3
|
+
Workspace = Terraspace::Terraform::Cloud::Workspace
|
4
|
+
|
5
|
+
desc "list", "List workspaces"
|
6
|
+
long_desc Help.text("cloud:list")
|
7
|
+
def list
|
8
|
+
Workspace.new(options).list
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "destroy STACK", "Destroy workspace"
|
12
|
+
long_desc Help.text("cloud:destroy")
|
13
|
+
option :yes, aliases: :y, type: :boolean, desc: "bypass are you sure prompt"
|
14
|
+
def destroy(mod)
|
15
|
+
Workspace.new(options.merge(mod: mod)).destroy
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "setup STACK", "Setup workspace"
|
19
|
+
long_desc Help.text("cloud:setup")
|
20
|
+
def setup(mod)
|
21
|
+
Workspace.new(options.merge(mod: mod)).setup
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -7,8 +7,15 @@ class Terraspace::CLI
|
|
7
7
|
|
8
8
|
# Commander always runs Build#run
|
9
9
|
def run
|
10
|
-
|
10
|
+
Terraspace::Builder.new(@options).run # generate and init
|
11
|
+
auto_create_backend
|
12
|
+
Init.new(@options.merge(calling_command: @name)).run
|
11
13
|
Terraspace::Terraform::Runner.new(@name, @options).run
|
12
14
|
end
|
15
|
+
|
16
|
+
def auto_create_backend
|
17
|
+
return unless @name == "apply"
|
18
|
+
Terraspace::Compiler::Backend.new(@mod).create
|
19
|
+
end
|
13
20
|
end
|
14
21
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
class Terraspace::CLI
|
4
|
+
class Init < Base
|
5
|
+
def initialize(options={})
|
6
|
+
# Original calling command. Can be from Commander which is a terraform command. IE: terraform apply
|
7
|
+
# Or can be from terraspace cloud setup. Which will be cloud-setup.
|
8
|
+
@calling_command = options[:calling_command]
|
9
|
+
super(options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
init if init?
|
14
|
+
build_remote_dependencies # runs after terraform init, which downloads remote modules
|
15
|
+
sync_cloud
|
16
|
+
end
|
17
|
+
|
18
|
+
# Note the init will always create the Terraform Cloud Workspace
|
19
|
+
def init
|
20
|
+
# default init timeout is pretty generous in case of slow internet to download the provider plugins
|
21
|
+
init_timeout = Integer(ENV['TS_INIT_TIMEOUT'] || 600)
|
22
|
+
Timeout::timeout(init_timeout) do
|
23
|
+
Terraspace::Terraform::Runner.new("init", @options).run if !auto? && @options[:init] != false # will run on @options[:init].nil?
|
24
|
+
end
|
25
|
+
rescue Timeout::Error
|
26
|
+
logger.error "ERROR: It took too long to run terraform init. Here is the output logs of terraform init:".color(:red)
|
27
|
+
logger.error IO.read(Terraspace::Terraform::Args::Default.terraform_init_log)
|
28
|
+
end
|
29
|
+
|
30
|
+
def sync_cloud
|
31
|
+
Terraspace::Terraform::Cloud.new(@options).run if %w[apply plan destroy cloud-setup].include?(@calling_command)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Currently only handles remote modules only one-level deep.
|
35
|
+
def build_remote_dependencies
|
36
|
+
modules_json_path = "#{@mod.cache_dir}/.terraform/modules/modules.json"
|
37
|
+
return unless File.exist?(modules_json_path)
|
38
|
+
|
39
|
+
initialized_modules = JSON.load(IO.read(modules_json_path))
|
40
|
+
# For example of structure see spec/fixtures/initialized/modules.json
|
41
|
+
initialized_modules["Modules"].each do |meta|
|
42
|
+
build_remote_mod(meta)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_remote_mod(meta)
|
47
|
+
return if local_source?(meta["Source"])
|
48
|
+
return if meta['Dir'] == '.' # root is already built
|
49
|
+
|
50
|
+
remote_mod = Mod::Remote.new(meta, @mod)
|
51
|
+
Compiler::Builder.new(remote_mod).build
|
52
|
+
end
|
53
|
+
|
54
|
+
def auto?
|
55
|
+
# command is only passed from CLI in the update specifically for this check
|
56
|
+
@options[:auto] && @calling_command == "apply"
|
57
|
+
end
|
58
|
+
private
|
59
|
+
def local_source?(s)
|
60
|
+
s =~ %r{^\.} || s =~ %r{^/}
|
61
|
+
end
|
62
|
+
|
63
|
+
def init?
|
64
|
+
%w[apply console destroy output plan providers refresh show validate cloud-setup].include?(@calling_command)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/terraspace/cli/new.rb
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
class Terraspace::CLI
|
2
2
|
class New < Terraspace::Command
|
3
|
+
long_desc Help.text(:git_hook)
|
4
|
+
GitHook.cli_options.each { |args| option(*args) }
|
5
|
+
register(GitHook, "git_hook", "git_hook", "Generates new git hook")
|
6
|
+
|
7
|
+
long_desc Help.text(:shim)
|
8
|
+
Shim.cli_options.each { |args| option(*args) }
|
9
|
+
register(Shim, "shim", "shim", "Generates terraspace shim")
|
10
|
+
|
3
11
|
long_desc Help.text(:module)
|
4
12
|
Module.base_options.each { |args| option(*args) }
|
5
13
|
Module.component_options.each { |args| option(*args) }
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Terraspace::CLI::New
|
2
|
+
class GitHook < Thor::Group
|
3
|
+
include Thor::Actions
|
4
|
+
|
5
|
+
def self.cli_options
|
6
|
+
[
|
7
|
+
[:envs, type: :array, default: %w[dev prod], desc: "envs to build"],
|
8
|
+
[:type, aliases: %w[t], default: "pre-push", desc: "git hook type"],
|
9
|
+
]
|
10
|
+
end
|
11
|
+
cli_options.each { |args| class_option(*args) }
|
12
|
+
|
13
|
+
def self.source_root
|
14
|
+
File.expand_path("../../../templates/base/git_hook", __dir__)
|
15
|
+
end
|
16
|
+
|
17
|
+
def create
|
18
|
+
return unless File.exist?(".git")
|
19
|
+
dest = ".git/hooks/#{options[:type]}"
|
20
|
+
template "hook.sh", dest
|
21
|
+
chmod dest, 0755
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def terraspace_build_commands
|
26
|
+
code = []
|
27
|
+
@options[:envs].each do |env|
|
28
|
+
code << %Q|TS_ENV=#{env} terraspace build placeholder|
|
29
|
+
end
|
30
|
+
code.join("\n")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
class Terraspace::CLI::New
|
2
|
+
class Shim < Thor::Group
|
3
|
+
include Thor::Actions
|
4
|
+
|
5
|
+
def self.cli_options
|
6
|
+
[
|
7
|
+
[:path, aliases: %w[p], default: "/usr/local/bin/terraspace", desc: "path to save the shim script"],
|
8
|
+
]
|
9
|
+
end
|
10
|
+
cli_options.each { |args| class_option(*args) }
|
11
|
+
|
12
|
+
def self.source_root
|
13
|
+
File.expand_path("../../../templates/base/shim", __dir__)
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_vars
|
17
|
+
@path = @options[:path]
|
18
|
+
end
|
19
|
+
|
20
|
+
def create
|
21
|
+
return unless File.exist?(".git")
|
22
|
+
dest = @path
|
23
|
+
template "terraspace", dest
|
24
|
+
chmod dest, 0755
|
25
|
+
end
|
26
|
+
|
27
|
+
def message
|
28
|
+
dir = File.dirname(@path)
|
29
|
+
puts <<~EOL
|
30
|
+
A terraspace shim as been generated at #{@path}
|
31
|
+
Please make sure that it is found in the $PATH.
|
32
|
+
|
33
|
+
You can double check with:
|
34
|
+
|
35
|
+
which terraspace
|
36
|
+
|
37
|
+
You should see
|
38
|
+
|
39
|
+
$ which terraspace
|
40
|
+
#{@path}
|
41
|
+
|
42
|
+
If you do not, please add #{dir} to your PATH.
|
43
|
+
You can usually do so by adding this line to ~/.bash_profile and opening a new terminal to check.
|
44
|
+
|
45
|
+
export PATH=#{dir}:/$PATH
|
46
|
+
|
47
|
+
EOL
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def switch_ruby_version_line
|
52
|
+
rbenv_installed = system("type rbenv 2>&1 > /dev/null")
|
53
|
+
if rbenv_installed
|
54
|
+
'eval "$(rbenv init -)"'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -11,26 +11,19 @@ class Terraspace::CLI
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def run
|
14
|
+
Terraspace.check_project!
|
14
15
|
build
|
15
16
|
puts "Summary of resources based on backend storage statefiles"
|
16
17
|
backend_expr = '.terraspace-cache/**/backend.*'
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
18
|
+
# Currently summary assumes backend are within the same bucket and key prefix
|
19
|
+
backend = Dir.glob(backend_expr).find { |p| p.include?("/#{Terraspace.env}/") }
|
20
|
+
process(backend) if backend
|
21
21
|
end
|
22
22
|
|
23
23
|
# Grab the last module and build that.
|
24
24
|
# Assume the backend key has the same prefix
|
25
25
|
def build
|
26
|
-
|
27
|
-
|
28
|
-
mod = @options[:mod]
|
29
|
-
unless mod
|
30
|
-
mod_path = Dir.glob("{app,vendor}/{modules,stacks}/*").last
|
31
|
-
mod = File.basename(mod_path)
|
32
|
-
end
|
33
|
-
Build.new(@options.merge(mod: mod)).run # generate and init
|
26
|
+
Build::Placeholder.new(@options).build
|
34
27
|
end
|
35
28
|
|
36
29
|
def process(path)
|
@@ -43,6 +36,10 @@ class Terraspace::CLI
|
|
43
36
|
|
44
37
|
info = backend.values.first # structure within the s3 or gcs key
|
45
38
|
klass = summary_class(name)
|
39
|
+
unless klass
|
40
|
+
logger.info "Summary is unavailable for this backend: #{name}"
|
41
|
+
exit
|
42
|
+
end
|
46
43
|
summary = klass.new(info, @options)
|
47
44
|
summary.call
|
48
45
|
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require "hcl_parser"
|
2
|
-
|
3
1
|
module Terraspace::Compiler
|
4
2
|
class Backend
|
5
3
|
extend Memoist
|
@@ -9,53 +7,27 @@ module Terraspace::Compiler
|
|
9
7
|
end
|
10
8
|
|
11
9
|
def create
|
12
|
-
klass =
|
10
|
+
klass = backend_interface(backend_name)
|
13
11
|
return unless klass # in case auto-creation is not supported for specific backend
|
14
12
|
|
15
|
-
|
16
|
-
|
13
|
+
interface = klass.new(backend_info)
|
14
|
+
interface.call
|
17
15
|
end
|
18
16
|
|
19
17
|
def backend_name
|
20
|
-
|
18
|
+
backend.keys.first # IE: s3, gcs, etc
|
21
19
|
end
|
22
20
|
|
23
21
|
def backend_info
|
24
|
-
|
25
|
-
end
|
26
|
-
|
27
|
-
def backend_raw
|
28
|
-
return {} unless exist?(backend_path)
|
29
|
-
if backend_path.include?('.json')
|
30
|
-
json_backend
|
31
|
-
else
|
32
|
-
hcl_backend
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def json_backend
|
37
|
-
data = JSON.load(IO.read(backend_path))
|
38
|
-
data.dig("terraform", "backend") || {}
|
39
|
-
end
|
40
|
-
|
41
|
-
def hcl_backend
|
42
|
-
return {} unless File.exist?(backend_path)
|
43
|
-
backend_raw = HclParser.load(IO.read(backend_path))
|
44
|
-
return {} unless backend_raw
|
45
|
-
backend_raw.dig("terraform", "backend") || {}
|
46
|
-
end
|
47
|
-
|
48
|
-
def exist?(path)
|
49
|
-
path && File.exist?(path)
|
22
|
+
backend.values.first # structure within the s3 or gcs key
|
50
23
|
end
|
51
24
|
|
52
|
-
def
|
53
|
-
|
54
|
-
Dir.glob(expr).first
|
25
|
+
def backend
|
26
|
+
Parser.new(@mod).result
|
55
27
|
end
|
56
|
-
memoize :
|
28
|
+
memoize :backend
|
57
29
|
|
58
|
-
def
|
30
|
+
def backend_interface(name)
|
59
31
|
return unless name
|
60
32
|
# IE: TerraspacePluginAws::Interfaces::Backend
|
61
33
|
klass_name = Terraspace::Plugin.klass("Backend", backend: name)
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "hcl_parser"
|
2
|
+
|
3
|
+
class Terraspace::Compiler::Backend
|
4
|
+
class Parser
|
5
|
+
extend Memoist
|
6
|
+
|
7
|
+
def initialize(mod)
|
8
|
+
@mod = mod
|
9
|
+
end
|
10
|
+
|
11
|
+
def result
|
12
|
+
return {} unless exist?(backend_path)
|
13
|
+
if backend_path.include?('.json')
|
14
|
+
json_backend
|
15
|
+
else
|
16
|
+
hcl_backend
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def json_backend
|
21
|
+
data = JSON.load(IO.read(backend_path))
|
22
|
+
data.dig("terraform", "backend") || {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def hcl_backend
|
26
|
+
return {} unless File.exist?(backend_path)
|
27
|
+
backend_raw = HclParser.load(IO.read(backend_path))
|
28
|
+
return {} unless backend_raw
|
29
|
+
backend_raw.dig("terraform", "backend") || {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def exist?(path)
|
33
|
+
path && File.exist?(path)
|
34
|
+
end
|
35
|
+
|
36
|
+
def backend_path
|
37
|
+
expr = "#{@mod.cache_dir}/backend.tf*"
|
38
|
+
Dir.glob(expr).first
|
39
|
+
end
|
40
|
+
memoize :backend_path
|
41
|
+
end
|
42
|
+
end
|
@@ -14,7 +14,7 @@ module Terraspace::Compiler
|
|
14
14
|
|
15
15
|
# build common config files: provider and backend for the root module
|
16
16
|
def build_config
|
17
|
-
return unless
|
17
|
+
return unless build?
|
18
18
|
build_config_templates
|
19
19
|
end
|
20
20
|
|
@@ -25,11 +25,15 @@ module Terraspace::Compiler
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def build_tfvars
|
28
|
-
return unless
|
28
|
+
return unless build?
|
29
29
|
Strategy::Tfvar.new(@mod).run # writer within Strategy to control file ordering
|
30
30
|
end
|
31
31
|
|
32
32
|
private
|
33
|
+
def build?
|
34
|
+
@mod.type == "stack" || @mod.root_module?
|
35
|
+
end
|
36
|
+
|
33
37
|
def build_config_templates
|
34
38
|
expr = "#{Terraspace.root}/config/terraform/**/*"
|
35
39
|
Dir.glob(expr).each do |path|
|
@@ -19,6 +19,7 @@ module Terraspace::Compiler
|
|
19
19
|
# only remove .tf* files. leaving cache .terraform and terraform.state files
|
20
20
|
def remove_materialized_artifacts
|
21
21
|
Dir.glob("#{Terraspace.cache_root}/**/*").each do |path|
|
22
|
+
next unless within_env?(path)
|
22
23
|
next if path.include?(".tfstate")
|
23
24
|
FileUtils.rm_f(path) if File.file?(path)
|
24
25
|
end
|
@@ -31,7 +32,7 @@ module Terraspace::Compiler
|
|
31
32
|
# the file again. With verbose logging, it shows it twice so that's a little bit confusing though.
|
32
33
|
#
|
33
34
|
# def remove_materialized_artifacts_dot_terraform
|
34
|
-
# expr = "#{@mod.
|
35
|
+
# expr = "#{@mod.cache_dir}/.terraform/**/*"
|
35
36
|
#
|
36
37
|
# Dir.glob(expr).each do |path|
|
37
38
|
# logger.info "path #{path}"
|
@@ -40,7 +41,23 @@ module Terraspace::Compiler
|
|
40
41
|
|
41
42
|
def remove_empty_directories
|
42
43
|
return unless File.exist?(Terraspace.cache_root)
|
43
|
-
Dir["#{Terraspace.cache_root}/**/"].reverse_each
|
44
|
+
Dir["#{Terraspace.cache_root}/**/"].reverse_each do |d|
|
45
|
+
next unless within_env?(d)
|
46
|
+
Dir.rmdir(d) if Dir.entries(d).size == 2
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Only remove files within an env for the TFC VCS-Workflow.
|
51
|
+
# We dont want to run:
|
52
|
+
#
|
53
|
+
# TS_ENV=prod terraspace up demo
|
54
|
+
#
|
55
|
+
# And that to delete the .terraspace-cache/us-west-2/dev files
|
56
|
+
#
|
57
|
+
# May need to allow further customization to this if user project has a stack named the same as the env.
|
58
|
+
#
|
59
|
+
def within_env?(path)
|
60
|
+
path.include?("/#{Terraspace.env}/")
|
44
61
|
end
|
45
62
|
end
|
46
63
|
end
|