terraspace 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +57 -30
  4. data/lib/templates/base/arg/terraform.rb.tt +3 -0
  5. data/lib/templates/base/helper/%name%_helper.rb.tt +2 -0
  6. data/lib/templates/base/hook/%kind%.rb.tt +7 -0
  7. data/lib/terraspace.rb +7 -4
  8. data/lib/terraspace/all/base.rb +0 -1
  9. data/lib/terraspace/app.rb +1 -0
  10. data/lib/terraspace/autoloader.rb +20 -3
  11. data/lib/terraspace/cli.rb +2 -0
  12. data/lib/terraspace/cli/all.rb +1 -1
  13. data/lib/terraspace/cli/bundle.rb +0 -1
  14. data/lib/terraspace/cli/clean/base.rb +0 -1
  15. data/lib/terraspace/cli/clean/cache.rb +0 -1
  16. data/lib/terraspace/cli/cloud.rb +2 -0
  17. data/lib/terraspace/cli/help/all/init.md +33 -0
  18. data/lib/terraspace/cli/help/logs.md +4 -4
  19. data/lib/terraspace/cli/help/new/arg.md +19 -0
  20. data/lib/terraspace/cli/help/new/helper.md +39 -0
  21. data/lib/terraspace/cli/help/new/hook.md +25 -0
  22. data/lib/terraspace/cli/help/new/test.md +34 -0
  23. data/lib/terraspace/cli/info.rb +12 -0
  24. data/lib/terraspace/cli/list.rb +2 -1
  25. data/lib/terraspace/cli/logs/concern.rb +1 -0
  26. data/lib/terraspace/cli/new.rb +22 -16
  27. data/lib/terraspace/cli/new/arg.rb +62 -0
  28. data/lib/terraspace/cli/new/helper.rb +44 -12
  29. data/lib/terraspace/cli/new/helpers.rb +22 -0
  30. data/lib/terraspace/cli/new/helpers/plugin_gem.rb +25 -0
  31. data/lib/terraspace/cli/new/hook.rb +70 -0
  32. data/lib/terraspace/cli/new/module.rb +0 -11
  33. data/lib/terraspace/cli/new/plugin.rb +4 -4
  34. data/lib/terraspace/cli/new/plugin/helper.rb +1 -1
  35. data/lib/terraspace/cli/new/project.rb +16 -7
  36. data/lib/terraspace/cli/new/sequence.rb +3 -10
  37. data/lib/terraspace/cli/new/source/core.rb +1 -1
  38. data/lib/terraspace/cli/new/stack.rb +1 -12
  39. data/lib/terraspace/cli/new/test.rb +50 -0
  40. data/lib/terraspace/cli/summary.rb +0 -1
  41. data/lib/terraspace/command.rb +16 -0
  42. data/lib/terraspace/compiler/builder.rb +2 -0
  43. data/lib/terraspace/compiler/dsl/mod.rb +2 -0
  44. data/lib/terraspace/compiler/dsl/syntax/mod.rb +2 -0
  45. data/lib/terraspace/compiler/dsl/syntax/mod/backend.rb +1 -1
  46. data/lib/terraspace/compiler/erb/context.rb +2 -0
  47. data/lib/terraspace/compiler/helper_extender.rb +27 -0
  48. data/lib/terraspace/core.rb +0 -6
  49. data/lib/terraspace/ext/core/module.rb +16 -0
  50. data/lib/terraspace/hooks/builder.rb +6 -7
  51. data/lib/terraspace/hooks/concern.rb +2 -2
  52. data/lib/terraspace/logger.rb +6 -0
  53. data/lib/terraspace/mod.rb +0 -1
  54. data/lib/terraspace/plugin.rb +8 -4
  55. data/lib/terraspace/plugin/config/interface.rb +2 -2
  56. data/lib/terraspace/plugin/helper/interface.rb +31 -0
  57. data/lib/terraspace/shell.rb +5 -33
  58. data/lib/terraspace/shell/error.rb +46 -0
  59. data/lib/terraspace/terraform/args/custom.rb +2 -3
  60. data/lib/terraspace/terraform/args/default.rb +9 -19
  61. data/lib/terraspace/terraform/runner.rb +6 -14
  62. data/lib/terraspace/terraform/runner/retryer.rb +69 -0
  63. data/lib/terraspace/version.rb +1 -1
  64. data/spec/terraspace/terraform/args/custom_spec.rb +6 -4
  65. data/terraspace.gemspec +4 -4
  66. metadata +30 -21
  67. data/lib/terraspace/cli/help/new/bootstrap_test.md +0 -8
  68. data/lib/terraspace/cli/help/new/module_test.md +0 -12
  69. data/lib/terraspace/cli/help/new/project_test.md +0 -8
  70. data/lib/terraspace/cli/new/helper/plugin_gem.rb +0 -12
  71. data/lib/terraspace/cli/new/test/base.rb +0 -17
  72. data/lib/terraspace/cli/new/test/bootstrap.rb +0 -18
  73. data/lib/terraspace/cli/new/test/module.rb +0 -15
  74. data/lib/terraspace/cli/new/test/project.rb +0 -15
@@ -1,5 +1,5 @@
1
1
  class Terraspace::CLI::New::Plugin
2
- module Helper
2
+ module Helpers
3
3
  private
4
4
  # helper
5
5
  def camel_name
@@ -4,16 +4,23 @@ class Terraspace::CLI::New
4
4
  [
5
5
  [:bundle, type: :boolean, default: true, desc: "Runs bundle install on the project"],
6
6
  [:config, type: :boolean, default: true, desc: "Whether or not to generate config files."],
7
- [:force, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files."],
8
- [:test_structure, type: :boolean, desc: "Create project bootstrap test structure."]
7
+ [:force, aliases: %w[y], type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files."],
8
+ [:quiet, type: :boolean, desc: "Quiet output."],
9
+ [:test_structure, type: :boolean, desc: "Create project bootstrap test structure."],
9
10
  ]
10
11
  end
11
12
 
12
13
  base_options.each { |args| class_option(*args) }
13
14
  project_options.each { |args| class_option(*args) }
14
15
 
16
+ private
17
+ def log(msg)
18
+ logger.info(msg) unless @options[:quiet]
19
+ end
20
+
21
+ public
15
22
  def creating_messaging
16
- puts "=> Creating new project called #{name}."
23
+ log "=> Creating new project called #{name}."
17
24
  end
18
25
 
19
26
  def create_base
@@ -60,15 +67,17 @@ class Terraspace::CLI::New
60
67
 
61
68
  def bundle_install
62
69
  return if @options[:bundle] == false
63
- puts "=> Installing dependencies with: bundle install"
70
+ log "=> Installing dependencies with: bundle install"
64
71
  Bundler.with_unbundled_env do
65
- system("BUNDLE_IGNORE_CONFIG=1 bundle install", chdir: name)
72
+ bundle = "BUNDLE_IGNORE_CONFIG=1 bundle install"
73
+ bundle << " > /dev/null 2>&1" if @options[:quiet]
74
+ system(bundle, chdir: name)
66
75
  end
67
76
  end
68
77
 
69
78
  def welcome_message_examples
70
79
  return unless options[:examples]
71
- puts <<~EOL
80
+ log <<~EOL
72
81
  #{"="*64}
73
82
  Congrats! You have successfully created a terraspace project.
74
83
  Check out the created files. Adjust to the examples and then deploy with:
@@ -83,7 +92,7 @@ class Terraspace::CLI::New
83
92
 
84
93
  def welcome_message_no_examples
85
94
  return if options[:examples]
86
- puts <<~EOL
95
+ log <<~EOL
87
96
  #{"="*64}
88
97
  Congrats! You have successfully created a terraspace project.
89
98
  Check out the created files.
@@ -3,12 +3,13 @@ require 'thor'
3
3
  class Terraspace::CLI::New
4
4
  class Sequence < Thor::Group
5
5
  include Thor::Actions
6
- include Helper
6
+ include Terraspace::Util::Logging
7
+ include Helpers
7
8
 
8
9
  def self.base_options
9
10
  [
10
11
  [:examples, type: :boolean, default: false, desc: "Also generate examples"],
11
- [:force, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"],
12
+ [:force, aliases: %w[y], type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"],
12
13
  [:lang, default: "hcl", desc: "Language to use: HCL/ERB or Ruby DSL"],
13
14
  [:plugin, aliases: %w[p], default: "aws", desc: "Cloud Plugin. Supports: aws, google"],
14
15
  [:test, type: :boolean, desc: "Whether or not to generate tests"],
@@ -47,13 +48,5 @@ class Terraspace::CLI::New
47
48
  source = Source::Plugin.new(self, @options)
48
49
  source.set_source_paths(template, type)
49
50
  end
50
-
51
- # A generator script hook to allow for further customizations
52
- # The dest folder like app/modules/demo is provided as a first argument to the command.
53
- def run_script(script, dest)
54
- command = "#{script} #{dest}"
55
- puts "Running: #{command}" unless ENV['TS_GENERATOR_MUTE']
56
- system(command)
57
- end
58
51
  end
59
52
  end
@@ -1,6 +1,6 @@
1
1
  module Terraspace::CLI::New::Source
2
2
  class Core
3
- include Terraspace::CLI::New::Helper::PluginGem
3
+ include Terraspace::CLI::New::Helpers::PluginGem
4
4
  include Terraspace::Util
5
5
 
6
6
  def initialize(sequence, options)
@@ -5,22 +5,11 @@ class Terraspace::CLI::New
5
5
  argument :name
6
6
 
7
7
  def create_stack
8
- plugin_template_source(@options[:lang], "stack") # IE: plugin_template_source("hcl", "stack")
9
-
10
8
  puts "=> Creating new stack called #{name}."
9
+ plugin_template_source(@options[:lang], "stack") # IE: plugin_template_source("hcl", "stack")
11
10
  dest = "app/stacks/#{name}"
12
11
  dest = "#{@options[:project_name]}/#{dest}" if @options[:project_name]
13
12
  directory ".", dest
14
13
  end
15
-
16
- def create_test
17
- Test::Project.start(component_args(name, @options[:project_name]))
18
- end
19
-
20
- def run_generator_hook_script
21
- script = ENV['TS_GENERATOR_STACK']
22
- return unless script
23
- run_script(script, "app/stacks/#{name}")
24
- end
25
14
  end
26
15
  end
@@ -0,0 +1,50 @@
1
+ class Terraspace::CLI::New
2
+ class Test < Thor::Group
3
+ include Thor::Actions
4
+ include Terraspace::CLI::New::Helpers
5
+
6
+ argument :name
7
+
8
+ def self.options
9
+ [
10
+ [:force, aliases: %w[y], type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"],
11
+ [:test_name, desc: "Test name. Defaults to the project, module or stack name"],
12
+ [:type, default: "project", desc: "project, stack or module"],
13
+ ]
14
+ end
15
+ options.each { |args| class_option(*args) }
16
+
17
+ private
18
+ def type
19
+ valid_types = %w[project stack module]
20
+ type = @options[:type]
21
+ valid_types.include?(type) ? type : "project" # fallback to project if user provides invalid type
22
+ end
23
+
24
+ def test_name
25
+ options[:test_name] || name
26
+ end
27
+
28
+ def dest
29
+ map = {
30
+ project: ".", # Terraspace.root
31
+ stack: "app/stacks/#{name}",
32
+ module: "app/modules/#{name}",
33
+ }
34
+ map[type.to_sym]
35
+ end
36
+
37
+ def test_template_source(template, type)
38
+ source = Terraspace::CLI::New::Source::Test.new(self, @options)
39
+ source.set_source_paths(template, type)
40
+ end
41
+
42
+ public
43
+
44
+ def create
45
+ test_template_source(@options[:lang], type)
46
+ puts "=> Creating #{type} test: #{name}"
47
+ directory ".", dest
48
+ end
49
+ end
50
+ end
@@ -11,7 +11,6 @@ class Terraspace::CLI
11
11
  end
12
12
 
13
13
  def run
14
- Terraspace.check_project!
15
14
  build_placeholder
16
15
  puts "Summary of resources based on backend storage statefiles"
17
16
  backend_expr = '.terraspace-cache/**/backend.*'
@@ -27,7 +27,11 @@ Thor::Util.singleton_class.prepend(ThorPrepend::Util)
27
27
  module Terraspace
28
28
  class Command < Thor
29
29
  class << self
30
+ include Terraspace::Util::Logging
31
+
30
32
  def dispatch(m, args, options, config)
33
+ check_project!(args.first)
34
+
31
35
  # Allow calling for help via:
32
36
  # terraspace command help
33
37
  # terraspace command -h
@@ -54,6 +58,18 @@ module Terraspace
54
58
  super
55
59
  end
56
60
 
61
+ def check_project!(command_name)
62
+ return if subcommand?
63
+ return if %w[-h -v completion completion_script help new test version].include?(command_name)
64
+ return if File.exist?("#{Terraspace.root}/config/app.rb")
65
+ logger.error "ERROR: It doesnt look like this is a terraspace project. Are you sure you are in a terraspace project?".color(:red)
66
+ ENV['TS_TEST'] ? raise : exit(1)
67
+ end
68
+
69
+ def subcommand?
70
+ !!caller.detect { |l| l.include?('in subcommand') }
71
+ end
72
+
57
73
  # Override command_help to include the description at the top of the
58
74
  # long_description.
59
75
  def command_help(shell, command_name)
@@ -73,6 +73,8 @@ module Terraspace::Compiler
73
73
  def skip?(src_path)
74
74
  return true unless File.file?(src_path)
75
75
  # certain folders will be skipped
76
+ src_path.include?("#{@mod.root}/config/args") ||
77
+ src_path.include?("#{@mod.root}/config/hooks") ||
76
78
  src_path.include?("#{@mod.root}/test")
77
79
  end
78
80
  end
@@ -1,8 +1,10 @@
1
1
  module Terraspace::Compiler::Dsl
2
2
  class Mod < Base
3
3
  include Syntax::Mod
4
+ include Terraspace::Compiler::HelperExtender
4
5
 
5
6
  def build
7
+ extend_module_level_helpers
6
8
  evaluate
7
9
  build_content
8
10
  end
@@ -3,5 +3,7 @@ module Terraspace::Compiler::Dsl::Syntax
3
3
  include Terraspace::Util::Logging
4
4
  include_dir("mod")
5
5
  include_dir("helpers")
6
+ include_plugin_helpers
7
+ include_project_level_helpers
6
8
  end
7
9
  end
@@ -11,7 +11,7 @@ module Terraspace::Compiler::Dsl::Syntax::Mod
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:
14
+ # Can set opts to explicitly use an specific backend. Example:
15
15
  #
16
16
  # opts = {backend: s3}
17
17
  #
@@ -1,11 +1,13 @@
1
1
  module Terraspace::Compiler::Erb
2
2
  class Context
3
3
  include Helpers
4
+ include Terraspace::Compiler::HelperExtender
4
5
 
5
6
  attr_reader :mod, :options
6
7
  def initialize(mod)
7
8
  @mod = mod
8
9
  @options = mod.options # so user has access to cli options
10
+ extend_module_level_helpers
9
11
  end
10
12
  end
11
13
  end
@@ -0,0 +1,27 @@
1
+ module Terraspace::Compiler
2
+ module HelperExtender
3
+ private
4
+ def extend_module_level_helpers
5
+ full_dir = "#{@mod.root}/config/helpers"
6
+ Dir.glob("#{full_dir}/**/*").each do |path|
7
+ regexp = Regexp.new(".*/helpers/")
8
+ klass = path.sub(regexp, '').sub('.rb','').camelize
9
+ klass = "#{mod_namespace}::#{klass}"
10
+ require path # able to use require instead of load since each helper has unique namespace
11
+ send :extend, klass.constantize
12
+ end
13
+ end
14
+
15
+ # IE: mod_namespace = Terraspace::Module::Demo
16
+ # Use separate namespaces scope with module name so custom helper methods from different modules are isolated.
17
+ def mod_namespace
18
+ mod_name = @mod.name.camelize
19
+ ns = "Terraspace::#{@mod.type.camelize}".constantize # IE: Terraspace::Module or Terraspace::Stack
20
+ if ns.const_defined?(mod_name.to_sym)
21
+ "#{ns}::#{mod_name}".constantize
22
+ else
23
+ ns.const_set(mod_name, Module.new)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -51,11 +51,5 @@ module Terraspace
51
51
  def logger=(v)
52
52
  @@logger = v
53
53
  end
54
-
55
- def check_project!
56
- return if File.exist?("#{Terraspace.root}/config/app.rb")
57
- logger.error "ERROR: It doesnt look like this is a terraspace project. Are you sure you are in a terraspace project?".color(:red)
58
- ENV['TS_TEST'] ? raise : exit(1)
59
- end
60
54
  end
61
55
  end
@@ -16,4 +16,20 @@ class Module
16
16
  include klass.constantize
17
17
  end
18
18
  end
19
+
20
+ def include_project_level_helpers
21
+ full_dir = "#{Terraspace.root}/config/helpers"
22
+ Dir.glob("#{full_dir}/**/*").each do |path|
23
+ regexp = Regexp.new(".*/config/helpers/")
24
+ klass = path.sub(regexp, '').sub('.rb','').camelize
25
+ klass = "Terraspace::Project::#{klass}"
26
+ include klass.constantize
27
+ end
28
+ end
29
+
30
+ def include_plugin_helpers
31
+ Terraspace::Plugin.helper_classes.each do |klass|
32
+ include klass # IE: TerraspacePluginAws::Interfaces::Helper
33
+ end
34
+ end
19
35
  end
@@ -7,14 +7,14 @@ module Terraspace::Hooks
7
7
 
8
8
  # IE: dsl_file: config/hooks/terraform.rb
9
9
  attr_accessor :name
10
- def initialize(mod, dsl_file, name)
11
- @mod, @dsl_file, @name = mod, dsl_file, name
10
+ def initialize(mod, file, name)
11
+ @mod, @file, @name = mod, file, name
12
12
  @hooks = {before: {}, after: {}}
13
13
  end
14
14
 
15
15
  def build
16
- return @hooks unless File.exist?(@dsl_file)
17
- evaluate_file(@dsl_file)
16
+ evaluate_file("#{Terraspace.root}/config/hooks/#{@file}")
17
+ evaluate_file("#{@mod.root}/config/hooks/#{@file}")
18
18
  @hooks.deep_stringify_keys!
19
19
  end
20
20
  memoize :build
@@ -37,11 +37,10 @@ module Terraspace::Hooks
37
37
  def run_hook(type, hook)
38
38
  return unless run?(hook)
39
39
 
40
- command = File.basename(@dsl_file).sub('.rb','') # IE: kubes, kubectl, docker
40
+ command = File.basename(@file).sub('.rb','') # IE: terraform or terraspace
41
41
  id = "#{command} #{type} #{@name}"
42
42
  label = " label: #{hook["label"]}" if hook["label"]
43
- logger.info "Running #{id} hook.#{label}"
44
- logger.debug "Hook options: #{hook}"
43
+ logger.info "Hook: Running #{id} hook.#{label}".color(:cyan)
45
44
  Runner.new(@mod, hook).run
46
45
  end
47
46
 
@@ -1,7 +1,7 @@
1
1
  module Terraspace::Hooks
2
2
  module Concern
3
- def run_hooks(dsl_file, name, &block)
4
- hooks = Builder.new(@mod, "#{Terraspace.root}/config/hooks/#{dsl_file}", name)
3
+ def run_hooks(file, name, &block)
4
+ hooks = Builder.new(@mod, file, name)
5
5
  hooks.build # build hooks
6
6
  hooks.run_hooks(&block)
7
7
  end
@@ -2,6 +2,12 @@ require 'logger'
2
2
 
3
3
  module Terraspace
4
4
  class Logger < ::Logger
5
+ def initialize(*args)
6
+ super
7
+ self.formatter = Formatter.new
8
+ self.level = :info
9
+ end
10
+
5
11
  def format_message(severity, datetime, progname, msg)
6
12
  line = if @logdev.dev == $stdout || @logdev.dev == $stderr
7
13
  msg # super simple format if stdout
@@ -31,7 +31,6 @@ module Terraspace
31
31
  end
32
32
 
33
33
  def check_exist!
34
- Terraspace.check_project!
35
34
  return if root
36
35
 
37
36
  pretty_paths = paths.map { |p| Terraspace::Util.pretty_path(p) }.join(", ")
@@ -16,14 +16,18 @@ module Terraspace
16
16
  @@meta
17
17
  end
18
18
 
19
- def layer_classes
20
- @@meta.map { |plugin, data| data[:layer_class] }.compact
21
- end
22
-
23
19
  def config_classes
24
20
  @@meta.map { |plugin, data| data[:config_class] }.compact
25
21
  end
26
22
 
23
+ def helper_classes
24
+ @@meta.map { |plugin, data| data[:helper_class] }.compact
25
+ end
26
+
27
+ def layer_classes
28
+ @@meta.map { |plugin, data| data[:layer_class] }.compact
29
+ end
30
+
27
31
  # The resource map can be used to customized the mapping from the resource "first word" to the plugin.
28
32
  #
29
33
  # resource map is in meta structure.
@@ -16,8 +16,8 @@ module Terraspace::Plugin::Config
16
16
  end
17
17
 
18
18
  def load_project_config
19
- project_config = "#{Terraspace.root}/config/plugins/#{provider}.rb"
20
- evaluate_file(project_config)
19
+ evaluate_file("#{Terraspace.root}/config/plugins/#{provider}.rb")
20
+ evaluate_file("#{Terraspace.root}/config/plugins/#{provider}/#{Terraspace.env}.rb")
21
21
  end
22
22
 
23
23
  def configure
@@ -0,0 +1,31 @@
1
+ module Terraspace::Plugin::Helper
2
+ module Interface
3
+ extend ActiveSupport::Concern
4
+
5
+ # Useful for plugin helpers. Can check this only run logic after dependency resolution.
6
+ def resolved?
7
+ !!@mod.resolved
8
+ end
9
+
10
+ class_methods do
11
+ @@helper_cache = {}
12
+ # This method is useful to avoid double call of heavy processing logic for tfvars,
13
+ # since the tfvars files get evaluated twice.
14
+ # Note: Not setting any cache or doing any logic unless resolved.
15
+ def cache_helper(meth)
16
+ uncached_meth = "uncached_#{meth}"
17
+ alias_method(uncached_meth, meth)
18
+ define_method(meth) do |*args|
19
+ return unless resolved? # return nil in first unresolved pass
20
+ id = Marshal.dump([meth] + args)
21
+ exist = @@helper_cache.key?(id)
22
+ if exist
23
+ @@helper_cache[id]
24
+ else
25
+ @@helper_cache[id] = send(uncached_meth, *args)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end