yle_tf 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +26 -0
  5. data/.travis.yml +7 -0
  6. data/CHANGELOG.md +3 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +442 -0
  11. data/Rakefile +6 -0
  12. data/bin/tf +7 -0
  13. data/examples/envs/prod.tfvars +5 -0
  14. data/examples/envs/test.tfvars +2 -0
  15. data/examples/main.tf +10 -0
  16. data/examples/tf.yaml +4 -0
  17. data/examples/tf_hooks/pre/get_current_hash.sh +26 -0
  18. data/examples/variables.tf +22 -0
  19. data/lib/yle_tf.rb +70 -0
  20. data/lib/yle_tf/action.rb +29 -0
  21. data/lib/yle_tf/action/builder.rb +9 -0
  22. data/lib/yle_tf/action/command.rb +25 -0
  23. data/lib/yle_tf/action/copy_root_module.rb +22 -0
  24. data/lib/yle_tf/action/generate_vars_file.rb +27 -0
  25. data/lib/yle_tf/action/load_config.rb +17 -0
  26. data/lib/yle_tf/action/terraform_init.rb +63 -0
  27. data/lib/yle_tf/action/tf_hooks.rb +48 -0
  28. data/lib/yle_tf/action/tmpdir.rb +35 -0
  29. data/lib/yle_tf/action/verify_terraform_version.rb +41 -0
  30. data/lib/yle_tf/action/verify_tf_env.rb +24 -0
  31. data/lib/yle_tf/backend_config.rb +41 -0
  32. data/lib/yle_tf/cli.rb +88 -0
  33. data/lib/yle_tf/config.rb +45 -0
  34. data/lib/yle_tf/config/defaults.rb +35 -0
  35. data/lib/yle_tf/config/erb.rb +22 -0
  36. data/lib/yle_tf/config/file.rb +26 -0
  37. data/lib/yle_tf/config/loader.rb +108 -0
  38. data/lib/yle_tf/error.rb +4 -0
  39. data/lib/yle_tf/logger.rb +48 -0
  40. data/lib/yle_tf/plugin.rb +55 -0
  41. data/lib/yle_tf/plugin/action_hook.rb +23 -0
  42. data/lib/yle_tf/plugin/loader.rb +59 -0
  43. data/lib/yle_tf/plugin/manager.rb +49 -0
  44. data/lib/yle_tf/system.rb +22 -0
  45. data/lib/yle_tf/tf_hook.rb +90 -0
  46. data/lib/yle_tf/tf_hook/runner.rb +48 -0
  47. data/lib/yle_tf/vars_file.rb +42 -0
  48. data/lib/yle_tf/version.rb +3 -0
  49. data/lib/yle_tf/version_requirement.rb +25 -0
  50. data/lib/yle_tf_plugins/backends/file/command.rb +31 -0
  51. data/lib/yle_tf_plugins/backends/file/config.rb +17 -0
  52. data/lib/yle_tf_plugins/backends/file/plugin.rb +16 -0
  53. data/lib/yle_tf_plugins/backends/s3/command.rb +19 -0
  54. data/lib/yle_tf_plugins/backends/s3/plugin.rb +16 -0
  55. data/lib/yle_tf_plugins/commands/__default/command.rb +14 -0
  56. data/lib/yle_tf_plugins/commands/__default/plugin.rb +14 -0
  57. data/lib/yle_tf_plugins/commands/_config/command.rb +11 -0
  58. data/lib/yle_tf_plugins/commands/_config/plugin.rb +19 -0
  59. data/lib/yle_tf_plugins/commands/_shell/command.rb +15 -0
  60. data/lib/yle_tf_plugins/commands/_shell/plugin.rb +14 -0
  61. data/lib/yle_tf_plugins/commands/help/command.rb +55 -0
  62. data/lib/yle_tf_plugins/commands/help/plugin.rb +18 -0
  63. data/lib/yle_tf_plugins/commands/version/command.rb +20 -0
  64. data/lib/yle_tf_plugins/commands/version/plugin.rb +18 -0
  65. data/vendor/hash_deep_merge.rb +59 -0
  66. data/vendor/logger_level_patch.rb +29 -0
  67. data/vendor/middleware/LICENSE +23 -0
  68. data/vendor/middleware/builder.rb +149 -0
  69. data/vendor/middleware/runner.rb +69 -0
  70. data/yle_tf.gemspec +37 -0
  71. metadata +160 -0
@@ -0,0 +1,48 @@
1
+ require 'forwardable'
2
+ require 'logger'
3
+ require 'rubygems'
4
+
5
+ class YleTf
6
+ # Logger for debug, error, etc. outputs.
7
+ # Prints to STDERR, so it does not mess with e.g. `terraform output`.
8
+ module Logger
9
+ class << self
10
+ extend Forwardable
11
+ def_delegators :logger, :debug, :info, :warn, :error, :fatal
12
+ def_delegators :logger, :debug?
13
+ end
14
+
15
+ def self.logger
16
+ @logger ||= ::Logger.new(STDERR).tap do |logger|
17
+ patch_for_old_ruby(logger)
18
+ logger.level = log_level
19
+ logger.formatter = log_formatter
20
+ end
21
+ end
22
+
23
+ def self.log_level
24
+ (ENV['TF_DEBUG'] && 'DEBUG') || \
25
+ ENV['TF_LOG'] || \
26
+ 'INFO'
27
+ end
28
+
29
+ def self.log_formatter
30
+ proc do |severity, _datetime, progname, msg|
31
+ if progname
32
+ "[#{progname}] #{severity}: #{msg}\n"
33
+ else
34
+ "#{severity}: #{msg}\n"
35
+ end
36
+ end
37
+ end
38
+
39
+ # Patches the `::Logger` in older Ruby versions to
40
+ # accept log level as a `String`
41
+ def self.patch_for_old_ruby(logger)
42
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
43
+ require_relative '../../vendor/logger_level_patch'
44
+ logger.extend(LoggerLevelPatch)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,55 @@
1
+ class YleTf
2
+ class Plugin
3
+ autoload :ActionHook, 'yle_tf/plugin/action_hook'
4
+ autoload :Loader, 'yle_tf/plugin/loader'
5
+ autoload :Manager, 'yle_tf/plugin/manager'
6
+
7
+ DEFAULT_COMMAND = Object.new.freeze
8
+
9
+ def self.manager
10
+ @manager ||= Manager.new
11
+ end
12
+
13
+ def self.register
14
+ Plugin.manager.register(self)
15
+ end
16
+
17
+ def self.action_hooks
18
+ @action_hooks ||= []
19
+ end
20
+
21
+ def self.action_hook(&block)
22
+ action_hooks << block
23
+ end
24
+
25
+ def self.commands
26
+ @commands ||= {}
27
+ end
28
+
29
+ def self.command(name, synopsis, &block)
30
+ name = name.to_s if name.is_a?(Symbol)
31
+ commands[name] = {
32
+ synopsis: synopsis,
33
+ proc: block
34
+ }
35
+ end
36
+
37
+ def self.default_config(config = nil)
38
+ @default_config = config if config
39
+ @default_config || {}
40
+ end
41
+
42
+ def self.config_context(context = nil)
43
+ @config_context = context if context
44
+ @config_context || {}
45
+ end
46
+
47
+ def self.backends
48
+ @backends ||= {}
49
+ end
50
+
51
+ def self.backend(type, &block)
52
+ backends[type.to_sym] = block
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,23 @@
1
+ class YleTf
2
+ class Plugin
3
+ class ActionHook
4
+ attr_reader :actions
5
+
6
+ def initialize(actions)
7
+ @actions = actions
8
+ end
9
+
10
+ def before(existing, new, *args, &block)
11
+ if actions.include?(existing)
12
+ actions.insert_before(existing, new, *args, &block)
13
+ end
14
+ end
15
+
16
+ def after(existing, new, *args, &block)
17
+ if actions.include?(existing)
18
+ actions.insert_after(existing, new, *args, &block)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,59 @@
1
+ require 'yle_tf/logger'
2
+
3
+ class YleTf
4
+ class Plugin
5
+ module Loader
6
+ BUNDLER_PLUGIN_GROUP = :tf_plugins
7
+
8
+ def self.load_plugins
9
+ load_core_plugins
10
+ load_bundler_plugins
11
+ load_user_plugins
12
+ end
13
+
14
+ def self.load_core_plugins
15
+ core_plugins.each do |plugin_file|
16
+ Logger.debug("Loading core plugin: #{File.basename(plugin_file, '.rb')}")
17
+ load(plugin_file)
18
+ end
19
+ end
20
+
21
+ def self.load_bundler_plugins
22
+ if defined?(Bundler)
23
+ print_bundler_plugin_list if Logger.debug?
24
+ Bundler.require(BUNDLER_PLUGIN_GROUP)
25
+ end
26
+ end
27
+
28
+ def self.load_user_plugins
29
+ user_plugins.each do |plugin|
30
+ Logger.debug("Loading user plugin: #{plugin}")
31
+ require(plugin)
32
+ end
33
+ end
34
+
35
+ def self.core_plugins
36
+ Dir.glob(File.expand_path('../../../yle_tf_plugins/**/plugin.rb', __FILE__))
37
+ end
38
+
39
+ def self.bundler_plugins
40
+ plugins = Bundler.definition.current_dependencies.select do |dep|
41
+ dep.groups.include?(BUNDLER_PLUGIN_GROUP)
42
+ end
43
+ plugins.map { |dep| Bundler.definition.specs[dep].first }
44
+ end
45
+
46
+ def self.user_plugins
47
+ ENV.fetch('TF_PLUGINS', '').split(/[ ,]+/)
48
+ end
49
+
50
+ def self.print_bundler_plugin_list
51
+ plugins = bundler_plugins
52
+ if !plugins.empty?
53
+ Logger.debug('Loading plugins via Bundler:')
54
+ plugins.each { |spec| Logger.debug(" - #{spec.name} = #{spec.version}") }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,49 @@
1
+ require 'yle_tf/logger'
2
+
3
+ class YleTf
4
+ class Plugin
5
+ class Manager
6
+ attr_reader :registered
7
+
8
+ def initialize
9
+ @registered = []
10
+ end
11
+
12
+ def register(plugin)
13
+ if !registered.include?(plugin)
14
+ Logger.debug("Registered plugin: #{plugin}")
15
+ @registered << plugin
16
+ end
17
+ end
18
+
19
+ def action_hooks
20
+ registered.map(&:action_hooks).flatten
21
+ end
22
+
23
+ def commands
24
+ {}.tap do |commands|
25
+ registered.each do |plugin|
26
+ commands.merge!(plugin.commands)
27
+ end
28
+ commands.default = commands.delete(DEFAULT_COMMAND)
29
+ end
30
+ end
31
+
32
+ def config_contexts
33
+ registered.map(&:config_context)
34
+ end
35
+
36
+ def default_configs
37
+ registered.map(&:default_config)
38
+ end
39
+
40
+ def backends
41
+ {}.tap do |backends|
42
+ registered.each do |plugin|
43
+ backends.merge!(plugin.backends)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,22 @@
1
+ require 'shellwords'
2
+
3
+ require 'yle_tf/error'
4
+ require 'yle_tf/logger'
5
+
6
+ class YleTf
7
+ # Helpers to execute system commands with error handling
8
+ #
9
+ # TODO: Add way to wrap stdout of the commands and direct it to `Logger`
10
+ class System
11
+ ExecuteError = Class.new(YleTf::Error)
12
+
13
+ def self.cmd(*args, **opts)
14
+ env = opts[:env]
15
+ YleTf::Logger.debug { "Calling `#{args.shelljoin}`#{" with env '#{env}'" if env}" }
16
+
17
+ system(env || {}, *args) ||
18
+ raise(ExecuteError,
19
+ "Failed to execute `#{args.shelljoin}`#{" with env '#{env}'" if env}")
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,90 @@
1
+ require 'fileutils'
2
+ require 'tmpdir'
3
+
4
+ require 'yle_tf/error'
5
+ require 'yle_tf/logger'
6
+ require 'yle_tf/system'
7
+
8
+ class YleTf
9
+ class TfHook
10
+ autoload :Runner, 'yle_tf/tf_hook/runner'
11
+
12
+ # Returns a `TfHook` instance from configuration hash
13
+ def self.from_config(config, tf_env)
14
+ TfHook.new(
15
+ description: config['description'],
16
+ source: config['source'],
17
+ vars: merge_vars(config['vars'], tf_env)
18
+ )
19
+ end
20
+
21
+ # Returns a `Hook` instance from a local file path
22
+ def self.from_file(path)
23
+ TfHook.new(
24
+ description: File.basename(path),
25
+ path: path
26
+ )
27
+ end
28
+
29
+ attr_reader :description, :source, :path, :vars
30
+
31
+ def initialize(opts = {})
32
+ @description = opts[:description]
33
+ @source = opts[:source]
34
+ @path = opts[:path]
35
+ @vars = opts[:vars] || {}
36
+ @tmpdir = nil
37
+ end
38
+
39
+ def run(tf_vars)
40
+ fetch if !path
41
+
42
+ Logger.info("Running hook '#{description}'...")
43
+ YleTf::System.cmd(path, env: vars.merge(tf_vars))
44
+ ensure
45
+ delete_tmpdir
46
+ end
47
+
48
+ def parse_source_config
49
+ m = %r{^(?<uri>.+)//(?<path>[^?]+)(\?ref=(?<ref>.*))?$}.match(source)
50
+ raise Error, "Invalid or missing `source` for hook '#{description}'" if !m
51
+
52
+ {
53
+ uri: m[:uri],
54
+ path: m[:path],
55
+ ref: m[:ref] || 'master'
56
+ }
57
+ end
58
+
59
+ def fetch
60
+ source_config = parse_source_config
61
+ source_config[:dir] = create_tmpdir
62
+ clone_git_repo(source_config)
63
+ @path = File.join(source_config[:dir], source_config[:path])
64
+ end
65
+
66
+ def clone_git_repo(config)
67
+ Logger.info("Cloning hook '#{description}' from #{config[:uri]} (#{config[:ref]})")
68
+ YleTf::System.cmd(
69
+ 'git', 'clone', '--no-progress', '--depth=1', '--branch', config[:ref],
70
+ '--', config[:uri], config[:dir]
71
+ )
72
+ end
73
+
74
+ def create_tmpdir
75
+ @tmpdir = Dir.mktmpdir('tf_hook_')
76
+ end
77
+
78
+ def delete_tmpdir
79
+ FileUtils.rm_r(@tmpdir) if @tmpdir
80
+ @tmpdir = nil
81
+ end
82
+
83
+ # Returns a hash with env specific vars merged into the default ones
84
+ def self.merge_vars(vars, tf_env)
85
+ vars ||= {}
86
+ defaults = vars['defaults'] || {}
87
+ defaults.merge(vars[tf_env] || {})
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,48 @@
1
+ require 'yle_tf/logger'
2
+ require 'yle_tf/tf_hook'
3
+
4
+ class YleTf
5
+ class TfHook
6
+ class Runner
7
+ attr_reader :config, :hook_env
8
+
9
+ def initialize(config, hook_env)
10
+ @config = config
11
+ @hook_env = hook_env
12
+ end
13
+
14
+ def tf_env
15
+ @tf_env ||= config.tf_env
16
+ end
17
+
18
+ def run(hook_type)
19
+ Logger.debug("Running #{hook_type} hooks")
20
+ hooks(hook_type).each do |hook|
21
+ hook.run(hook_env)
22
+ end
23
+ end
24
+
25
+ def hooks(hook_type)
26
+ hook_confs(hook_type).map { |conf| TfHook.from_config(conf, tf_env) } +
27
+ hook_files(hook_type).map { |file| TfHook.from_file(file) }
28
+ end
29
+
30
+ def hook_confs(hook_type)
31
+ config.fetch('hooks', hook_type).select do |hook|
32
+ if hook['envs'] && !hook['envs'].include?(tf_env)
33
+ Logger.debug("Skipping hook '#{hook['description']}' in env '#{tf_env}'")
34
+ false
35
+ else
36
+ true
37
+ end
38
+ end
39
+ end
40
+
41
+ def hook_files(hook_type)
42
+ Dir.glob("tf_hooks/#{hook_type}/*").select do |file|
43
+ File.executable?(file) && !File.directory?(file)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,42 @@
1
+ class YleTf
2
+ class VarsFile
3
+ # Returns the env specific tfvars file path if it exists
4
+ def self.find_env_vars_file(config)
5
+ path = "#{config.module_dir}/envs/#{config.tf_env}.tfvars"
6
+ VarsFile.new(path) if File.exist?(path)
7
+ end
8
+
9
+ # Returns all envs that have tfvars files
10
+ def self.list_all_envs(config)
11
+ Dir.glob("#{config.module_dir}/envs/*.tfvars").map do |path|
12
+ File.basename(path, '.tfvars')
13
+ end
14
+ end
15
+
16
+ attr_reader :path
17
+
18
+ def initialize(path)
19
+ @path = path
20
+ end
21
+
22
+ def read
23
+ IO.read(path)
24
+ end
25
+
26
+ def append_file(vars_file)
27
+ File.open(path, 'a') do |file|
28
+ file.puts # ensure we don't append to an existing line
29
+ file.puts(vars_file.read)
30
+ end
31
+ end
32
+
33
+ def append_vars(vars)
34
+ File.open(path, 'a') do |file|
35
+ file.puts # ensure we don't append to an existing line
36
+ vars.each do |key, value|
37
+ file.puts %(#{key} = "#{value}")
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end