terraspace 0.1.1 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -2
  3. data/README.md +20 -12
  4. data/lib/templates/base/git_hook/hook.sh +5 -0
  5. data/lib/templates/base/project/.gitignore +0 -1
  6. data/lib/templates/base/shim/terraspace +7 -0
  7. data/lib/templates/hcl/project/config/terraform/backend.tf.tt +3 -3
  8. data/lib/templates/plugin/lib/templates/hcl/project/config/terraform/backend.tf.tt +2 -2
  9. data/lib/terraspace/app.rb +8 -0
  10. data/lib/terraspace/builder.rb +6 -45
  11. data/lib/terraspace/cli.rb +34 -18
  12. data/lib/terraspace/cli/build/placeholder.rb +40 -0
  13. data/lib/terraspace/cli/cloud.rb +24 -0
  14. data/lib/terraspace/cli/commander.rb +8 -1
  15. data/lib/terraspace/cli/init.rb +67 -0
  16. data/lib/terraspace/cli/list.rb +13 -0
  17. data/lib/terraspace/cli/new.rb +8 -0
  18. data/lib/terraspace/cli/new/git_hook.rb +33 -0
  19. data/lib/terraspace/cli/new/shim.rb +58 -0
  20. data/lib/terraspace/cli/summary.rb +9 -12
  21. data/lib/terraspace/compiler/backend.rb +9 -37
  22. data/lib/terraspace/compiler/backend/parser.rb +42 -0
  23. data/lib/terraspace/compiler/builder.rb +6 -2
  24. data/lib/terraspace/compiler/cleaner.rb +19 -2
  25. data/lib/terraspace/compiler/cleaner/backend_change.rb +1 -1
  26. data/lib/terraspace/compiler/dsl/syntax/mod.rb +1 -0
  27. data/lib/terraspace/compiler/dsl/syntax/mod/backend.rb +16 -3
  28. data/lib/terraspace/compiler/expander.rb +28 -1
  29. data/lib/terraspace/compiler/writer.rb +1 -1
  30. data/lib/terraspace/core.rb +7 -1
  31. data/lib/terraspace/mod.rb +37 -12
  32. data/lib/terraspace/mod/remote.rb +1 -1
  33. data/lib/terraspace/plugin/expander/interface.rb +48 -5
  34. data/lib/terraspace/plugin/infer_provider.rb +15 -0
  35. data/lib/terraspace/plugin/layer/interface.rb +5 -0
  36. data/lib/terraspace/plugin/summary/interface.rb +1 -0
  37. data/lib/terraspace/seeder.rb +4 -4
  38. data/lib/terraspace/terraform/api.rb +58 -0
  39. data/lib/terraspace/terraform/api/client.rb +10 -0
  40. data/lib/terraspace/terraform/api/http.rb +106 -0
  41. data/lib/terraspace/terraform/api/var.rb +72 -0
  42. data/lib/terraspace/terraform/api/vars.rb +38 -0
  43. data/lib/terraspace/terraform/api/vars/base.rb +7 -0
  44. data/lib/terraspace/terraform/api/vars/json.rb +14 -0
  45. data/lib/terraspace/terraform/api/vars/rb.rb +21 -0
  46. data/lib/terraspace/terraform/args/custom.rb +1 -1
  47. data/lib/terraspace/terraform/args/default.rb +16 -2
  48. data/lib/terraspace/terraform/cloud.rb +25 -0
  49. data/lib/terraspace/terraform/cloud/workspace.rb +95 -0
  50. data/lib/terraspace/terraform/runner.rb +1 -1
  51. data/lib/terraspace/util/sh.rb +1 -1
  52. data/lib/terraspace/version.rb +1 -1
  53. data/spec/fixtures/{cache_build_dir → cache_dir}/variables.tf +0 -0
  54. data/spec/fixtures/projects/hcl/aws/config/backend.tf +1 -1
  55. data/spec/fixtures/projects/hcl/google/config/backend.tf +1 -1
  56. data/spec/terraspace/seeder_spec.rb +1 -1
  57. data/spec/terraspace/terraform/hooks/builder_spec.rb +1 -1
  58. data/terraspace.gemspec +5 -4
  59. metadata +47 -13
  60. data/lib/terraspace/cli/build.rb +0 -7
@@ -27,7 +27,7 @@ class Terraspace::Compiler::Cleaner
27
27
  end
28
28
 
29
29
  def current_backend
30
- materialized_path = find_src_path("#{@mod.cache_build_dir}/backend*")
30
+ materialized_path = find_src_path("#{@mod.cache_dir}/backend*")
31
31
  IO.read(materialized_path) if materialized_path
32
32
  end
33
33
 
@@ -1,5 +1,6 @@
1
1
  module Terraspace::Compiler::Dsl::Syntax
2
2
  module Mod
3
+ include Terraspace::Util::Logging
3
4
  include_dir("mod")
4
5
  include_dir("helpers")
5
6
  end
@@ -3,16 +3,29 @@ module Terraspace::Compiler::Dsl::Syntax::Mod
3
3
  def backend(name, props={})
4
4
  terraform = @structure[:terraform] ||= {}
5
5
  backend = terraform[:backend] ||= {}
6
- backend_expand_all!(name, props)
6
+ expansion_all!(name, props)
7
7
  backend[name] = props
8
8
  end
9
9
 
10
- def backend_expand_all!(backend_name, props={})
10
+ def expansion_all!(backend_name, props={})
11
11
  Terraspace::Compiler::Expander.new(@mod, backend_name).expand(props)
12
12
  end
13
13
 
14
+ # Can set opts to explicitly use an specfic backend. Example:
15
+ #
16
+ # opts = {backend: s3}
17
+ #
18
+ # Else Terraspace autodetects the backend installed.
19
+ #
20
+ def expansion(string, opts={})
21
+ expander = Terraspace::Compiler::Expander.autodetect(@mod, opts)
22
+ expander.expansion(string)
23
+ end
24
+
25
+ # DEPRECATED: Will be removed in future release
14
26
  def backend_expand(backend_name, string)
15
- Terraspace::Compiler::Expander.new(@mod, backend_name).expand_string(string)
27
+ logger.info "DEPRECATED backend_expand: instead use expansion(string)"
28
+ Terraspace::Compiler::Expander.new(@mod, backend_name).expansion(string)
16
29
  end
17
30
  end
18
31
  end
@@ -1,6 +1,6 @@
1
1
  module Terraspace::Compiler
2
2
  class Expander
3
- delegate :expand, :expand_string, to: :expander
3
+ delegate :expand, :expansion, to: :expander
4
4
 
5
5
  attr_reader :expander
6
6
  def initialize(mod, name)
@@ -15,5 +15,32 @@ module Terraspace::Compiler
15
15
  rescue NameError
16
16
  Terraspace::Plugin::Expander::Generic
17
17
  end
18
+
19
+ class << self
20
+ extend Memoist
21
+
22
+ def autodetect(mod, opts={})
23
+ backend = opts[:backend]
24
+ unless backend
25
+ plugin = find_plugin
26
+ backend = plugin[:backend]
27
+ end
28
+ new(mod, backend)
29
+ end
30
+ memoize :autodetect
31
+
32
+ def find_plugin
33
+ plugins = Terraspace::Plugin.meta
34
+ if plugins.size == 1
35
+ plugins.first[1]
36
+ else
37
+ precedence = %w[aws azurerm google]
38
+ plugin = precedence.find do |provider|
39
+ plugins[provider]
40
+ end
41
+ plugins[plugin]
42
+ end
43
+ end
44
+ end
18
45
  end
19
46
  end
@@ -19,7 +19,7 @@ module Terraspace::Compiler
19
19
  if @mod.is_a?(Terraspace::Mod::Remote)
20
20
  File.dirname(@src_path) # for Mod::Remote src is dest
21
21
  else
22
- @mod.cache_build_dir
22
+ @mod.cache_dir
23
23
  end
24
24
  end
25
25
 
@@ -18,7 +18,7 @@ module Terraspace
18
18
  end
19
19
 
20
20
  def cache_root
21
- ENV['TS_CACHE_ROOT'] || "#{root}/.terraspace-cache"
21
+ ENV['TS_CACHE_ROOT'] || config.build.cache_root || "#{root}/.terraspace-cache"
22
22
  end
23
23
  memoize :cache_root
24
24
 
@@ -42,5 +42,11 @@ module Terraspace
42
42
  config.logger
43
43
  end
44
44
  memoize :logger
45
+
46
+ def check_project!
47
+ return if File.exist?("#{Terraspace.root}/config/app.rb")
48
+ logger.error "ERROR: It doesnt look like this is a terraspace project. Are you sure you are in a terraspace project?".color(:red)
49
+ ENV['TS_TEST'] ? raise : exit(1)
50
+ end
45
51
  end
46
52
  end
@@ -11,18 +11,26 @@ module Terraspace
11
11
 
12
12
  attr_reader :name, :consider_stacks, :instance, :options
13
13
  def initialize(name, options={})
14
- @name, @options = name, options
14
+ @name, @options = placeholder(name), options
15
15
  @consider_stacks = options[:consider_stacks].nil? ? true : options[:consider_stacks]
16
16
  @instance = options[:instance]
17
17
  end
18
18
 
19
+ def placeholder(name)
20
+ if name == "placeholder"
21
+ Terraspace::CLI::Build::Placeholder.new(@options).find_mod
22
+ else
23
+ name
24
+ end
25
+ end
26
+
19
27
  attr_accessor :root_module
20
28
  def root_module?
21
29
  @root_module
22
30
  end
23
31
 
24
32
  def check_exist!
25
- check_terraspace_project!
33
+ Terraspace.check_project!
26
34
  return if root
27
35
 
28
36
  pretty_paths = paths.map { |p| Terraspace::Util.pretty_path(p) }
@@ -30,16 +38,10 @@ module Terraspace
30
38
  ENV['TS_TEST'] ? raise : exit(1)
31
39
  end
32
40
 
33
- def check_terraspace_project!
34
- return if File.exist?("#{Terraspace.root}/config/app.rb")
35
- logger.error "ERROR: It doesnt look like this is a terraspace project. Are you sure you are in a terraspace project?".color(:red)
36
- ENV['TS_TEST'] ? raise : exit(1)
37
- end
38
-
39
41
  def to_info
40
42
  {
41
43
  build_dir: build_dir,
42
- cache_build_dir: Terraspace::Util.pretty_path(cache_build_dir),
44
+ cache_dir: Terraspace::Util.pretty_path(cache_dir),
43
45
  name: name,
44
46
  root: Terraspace::Util.pretty_path(root),
45
47
  type: type,
@@ -48,10 +50,30 @@ module Terraspace
48
50
  end
49
51
 
50
52
  def root
51
- paths.find { |p| File.exist?(p) }
53
+ root = paths.find { |p| File.exist?(p) }
54
+ if root.nil?
55
+ possible_fake_root
56
+ else
57
+ root
58
+ end
52
59
  end
53
60
  memoize :root
54
61
 
62
+ # If the app/stacks/NAME has been removed in source code but stack still exist in the cloud.
63
+ # allow user to delete by materializing an empty stack with the backend.tf
64
+ # Note this does not seem to work for Terraform Cloud as terraform init doesnt seem to download the plugins
65
+ # required. SIt only works for s3, azurerm, and gcs backends. On TFC, you can delete the stack via the GUI though.
66
+ #
67
+ # down - so user can delete stacks w/o needing to create an empty app/stacks/demo folder
68
+ # null - for the terraspace summary command when there are zero stacks.
69
+ # Also useful for terraspace cloud list_workspaces
70
+ #
71
+ def possible_fake_root
72
+ if @options[:command] == "down"
73
+ "#{Terraspace.root}/app/stacks/#{@name}" # fake stack root
74
+ end
75
+ end
76
+
55
77
  # Relative folder path without app or vendor. For example, the actual location can be found in a couple of places
56
78
  #
57
79
  # app/modules/vpc
@@ -74,9 +96,12 @@ module Terraspace
74
96
  end
75
97
 
76
98
  # Full path with build_dir
77
- def cache_build_dir
78
- "#{Terraspace.cache_root}/#{Terraspace.env}/#{build_dir}"
99
+ def cache_dir
100
+ pattern = Terraspace.config.build.cache_dir # IE: :CACHE_ROOT/:REGION/:ENV/:BUILD_DIR
101
+ expander = Terraspace::Compiler::Expander.autodetect(self)
102
+ expander.expansion(pattern)
79
103
  end
104
+ memoize :cache_dir
80
105
 
81
106
  def type
82
107
  root.include?("/stacks/") ? "stack" : "module"
@@ -8,7 +8,7 @@ class Terraspace::Mod
8
8
  end
9
9
 
10
10
  def root
11
- "#{@parent.cache_build_dir}/#{@meta['Dir']}"
11
+ "#{@parent.cache_dir}/#{@meta['Dir']}"
12
12
  end
13
13
 
14
14
  def type
@@ -6,34 +6,64 @@
6
6
  #
7
7
  module Terraspace::Plugin::Expander
8
8
  module Interface
9
- delegate :build_dir, :type_dir, to: :mod
9
+ include Terraspace::Plugin::InferProvider
10
+
11
+ delegate :build_dir, :type_dir, :type, to: :mod
10
12
 
11
13
  attr_reader :mod
12
14
  def initialize(mod)
13
15
  @mod = mod
14
16
  end
15
17
 
18
+ # Handles list of objects. Calls expansion to handle each string expansion.
16
19
  def expand(props={})
17
20
  props.each do |key, value|
18
- props[key] = expand_string(value)
21
+ props[key] = expansion(value)
19
22
  end
20
23
  props
21
24
  end
22
25
 
26
+ # Handles single string
27
+ #
23
28
  # Replaces variables denoted by colon in front with actual values. Example:
24
29
  #
25
30
  # :REGION/:ENV/:BUILD_DIR/terraform.tfstate
26
31
  # =>
27
32
  # us-west-2/dev/stacks/wordpress/terraform.tfstate
28
33
  #
29
- def expand_string(string)
34
+ def expansion(string)
30
35
  return string unless string.is_a?(String) # in case of nil
31
36
 
37
+ string = string.dup
32
38
  vars = string.scan(/:\w+/) # => [":ENV", ":BUILD_DIR"]
33
39
  vars.each do |var|
34
40
  string.gsub!(var, var_value(var))
35
41
  end
36
- string
42
+ strip(string)
43
+ end
44
+
45
+ # remove leading and trailing common separators.
46
+ #
47
+ # This is useful for when INSTANCE is not set.
48
+ # Note: BUILD_DIR includes INSTANCE
49
+ #
50
+ # Examples:
51
+ #
52
+ # cache_dir:
53
+ #
54
+ # :CACHE_ROOT/:REGION/:ENV/:BUILD_DIR/
55
+ #
56
+ # s3 backend key:
57
+ #
58
+ # :REGION/:ENV/:BUILD_DIR/terraform.tfstate
59
+ #
60
+ # workspace:
61
+ #
62
+ # :MOD_NAME-:ENV-:REGION-:INSTANCE
63
+ #
64
+ def strip(string)
65
+ string.sub(/^-+/,'').sub(/-+$/,'') # remove leading and trailing -
66
+ .sub(%r{/+$},'') # only remove trailing / or else /home/ec2-user => home/ec2-user
37
67
  end
38
68
 
39
69
  def var_value(name)
@@ -48,5 +78,18 @@ module Terraspace::Plugin::Expander
48
78
  def env
49
79
  Terraspace.env
50
80
  end
81
+
82
+ def type_instance
83
+ [type, instance].reject { |s| s.blank? }.join('-')
84
+ end
85
+
86
+ def instance
87
+ @mod.options[:instance] || ''
88
+ end
89
+ alias_method :instance_option, :instance
90
+
91
+ def cache_root
92
+ Terraspace.cache_root
93
+ end
51
94
  end
52
- end
95
+ end
@@ -0,0 +1,15 @@
1
+ module Terraspace::Plugin
2
+ module InferProvider
3
+ # Examples:
4
+ # TerraspacePluginAws => aws
5
+ # TerraspacePluginAzurerm => azurerm
6
+ # TerraspacePluginGoogle => google
7
+ #
8
+ # If multiple clouds used in a single Terraspace project. The TS_PROVIDER_EXPANSION env var provides a way to
9
+ # change it. Can possibly use config hooks to set different values based on the module being deployed:
10
+ # https://terraspace.cloud/docs/config/hooks/
11
+ def provider
12
+ ENV['TS_PROVIDER'] || self.class.to_s.split('::').first.sub('TerraspacePlugin','').underscore
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module Terraspace::Plugin::Layer
2
+ module Interface
3
+ include Terraspace::Plugin::InferProvider
4
+ end
5
+ end
@@ -104,6 +104,7 @@ module Terraspace::Plugin::Summary
104
104
  index = path.index(regexp)
105
105
  unless index
106
106
  logger.error "ERROR: Unable to find the #{Terraspace.env} position in the prefix"
107
+ logger.error "path used: #{path}"
107
108
  exit 1
108
109
  end
109
110
  env_chars = Terraspace.env.size + 1
@@ -21,7 +21,7 @@ module Terraspace
21
21
  elsif exist?("variables.tf.json")
22
22
  JSON.load(read("variables.tf.json"))
23
23
  else
24
- logger.warn "WARN: no variables.tf or variables.tf.json found in: #{@mod.cache_build_dir}"
24
+ logger.warn "WARN: no variables.tf or variables.tf.json found in: #{@mod.cache_dir}"
25
25
  ENV['TS_TEST'] ? raise : exit
26
26
  end
27
27
  end
@@ -30,7 +30,7 @@ module Terraspace
30
30
  def load_hcl_variables
31
31
  HclParser.load(read("variables.tf"))
32
32
  rescue Racc::ParseError => e
33
- logger.error "ERROR: Unable to parse the #{Util.pretty_path(@mod.cache_build_dir)}/variables.tf file".color(:red)
33
+ logger.error "ERROR: Unable to parse the #{Util.pretty_path(@mod.cache_dir)}/variables.tf file".color(:red)
34
34
  logger.error "and generate the starter tfvars file. This is probably due to a complex variable type."
35
35
  logger.error "#{e.class}: #{e.message}"
36
36
  puts
@@ -48,12 +48,12 @@ module Terraspace
48
48
  memoize :dest_path
49
49
 
50
50
  def exist?(file)
51
- path = "#{@mod.cache_build_dir}/#{file}"
51
+ path = "#{@mod.cache_dir}/#{file}"
52
52
  File.exist?(path)
53
53
  end
54
54
 
55
55
  def read(file)
56
- path = "#{@mod.cache_build_dir}/#{file}"
56
+ path = "#{@mod.cache_dir}/#{file}"
57
57
  logger.info "Reading: #{Util.pretty_path(path)}"
58
58
  IO.read(path)
59
59
  end
@@ -0,0 +1,58 @@
1
+ module Terraspace::Terraform
2
+ class Api
3
+ extend Memoist
4
+ include Client
5
+ include Terraspace::Util::Logging
6
+
7
+ def initialize(mod, remote)
8
+ @mod = mod
9
+ @organization = remote['organization']
10
+ @workspace_name = remote['workspaces']['name']
11
+ end
12
+
13
+ # Docs: https://www.terraform.io/docs/cloud/api/workspaces.html
14
+ def set_working_dir
15
+ return if working_directory == workspace['attributes']['working-directory']
16
+
17
+ payload = {
18
+ data: {
19
+ attributes: {
20
+ "working-directory": working_directory
21
+ },
22
+ type: "workspaces"
23
+ }
24
+ }
25
+ http.patch("organizations/#{@organization}/workspaces/#{@workspace_name}", payload)
26
+ end
27
+
28
+ def working_directory
29
+ cache_dir = @mod.cache_dir.sub("#{Terraspace.root}/", '')
30
+ relative_root = Terraspace.config.cloud.relative_root # prepended to TFC Working Directory
31
+ relative_root ? "#{relative_root}/#{cache_dir}" : cache_dir
32
+ end
33
+
34
+ def set_env_vars
35
+ Vars.new(@mod, workspace).run
36
+ end
37
+
38
+ def workspace(options={})
39
+ payload = http.get("organizations/#{@organization}/workspaces/#{@workspace_name}")
40
+ # Note only way to get here is to bypass init. Example:
41
+ #
42
+ # terraspace up demo --no-init
43
+ #
44
+ unless payload || options[:exit_on_fail] == false
45
+ logger.error "ERROR: Unable to find the workspace. The workspace may not exist. Or the Terraform token may be invalid. Please double check your Terraform token.".color(:red)
46
+ exit 1
47
+ end
48
+ return unless payload
49
+ payload['data']
50
+ end
51
+ memoize :workspace
52
+
53
+ def destroy_workspace
54
+ # resp payload from delete operation is nil
55
+ http.delete("/organizations/#{@organization}/workspaces/#{@workspace_name}")
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,10 @@
1
+ class Terraspace::Terraform::Api
2
+ module Client
3
+ extend Memoist
4
+
5
+ def http
6
+ Http.new
7
+ end
8
+ memoize :http
9
+ end
10
+ end