yle_tf 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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