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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +1 -0
- data/.rubocop.yml +26 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +442 -0
- data/Rakefile +6 -0
- data/bin/tf +7 -0
- data/examples/envs/prod.tfvars +5 -0
- data/examples/envs/test.tfvars +2 -0
- data/examples/main.tf +10 -0
- data/examples/tf.yaml +4 -0
- data/examples/tf_hooks/pre/get_current_hash.sh +26 -0
- data/examples/variables.tf +22 -0
- data/lib/yle_tf.rb +70 -0
- data/lib/yle_tf/action.rb +29 -0
- data/lib/yle_tf/action/builder.rb +9 -0
- data/lib/yle_tf/action/command.rb +25 -0
- data/lib/yle_tf/action/copy_root_module.rb +22 -0
- data/lib/yle_tf/action/generate_vars_file.rb +27 -0
- data/lib/yle_tf/action/load_config.rb +17 -0
- data/lib/yle_tf/action/terraform_init.rb +63 -0
- data/lib/yle_tf/action/tf_hooks.rb +48 -0
- data/lib/yle_tf/action/tmpdir.rb +35 -0
- data/lib/yle_tf/action/verify_terraform_version.rb +41 -0
- data/lib/yle_tf/action/verify_tf_env.rb +24 -0
- data/lib/yle_tf/backend_config.rb +41 -0
- data/lib/yle_tf/cli.rb +88 -0
- data/lib/yle_tf/config.rb +45 -0
- data/lib/yle_tf/config/defaults.rb +35 -0
- data/lib/yle_tf/config/erb.rb +22 -0
- data/lib/yle_tf/config/file.rb +26 -0
- data/lib/yle_tf/config/loader.rb +108 -0
- data/lib/yle_tf/error.rb +4 -0
- data/lib/yle_tf/logger.rb +48 -0
- data/lib/yle_tf/plugin.rb +55 -0
- data/lib/yle_tf/plugin/action_hook.rb +23 -0
- data/lib/yle_tf/plugin/loader.rb +59 -0
- data/lib/yle_tf/plugin/manager.rb +49 -0
- data/lib/yle_tf/system.rb +22 -0
- data/lib/yle_tf/tf_hook.rb +90 -0
- data/lib/yle_tf/tf_hook/runner.rb +48 -0
- data/lib/yle_tf/vars_file.rb +42 -0
- data/lib/yle_tf/version.rb +3 -0
- data/lib/yle_tf/version_requirement.rb +25 -0
- data/lib/yle_tf_plugins/backends/file/command.rb +31 -0
- data/lib/yle_tf_plugins/backends/file/config.rb +17 -0
- data/lib/yle_tf_plugins/backends/file/plugin.rb +16 -0
- data/lib/yle_tf_plugins/backends/s3/command.rb +19 -0
- data/lib/yle_tf_plugins/backends/s3/plugin.rb +16 -0
- data/lib/yle_tf_plugins/commands/__default/command.rb +14 -0
- data/lib/yle_tf_plugins/commands/__default/plugin.rb +14 -0
- data/lib/yle_tf_plugins/commands/_config/command.rb +11 -0
- data/lib/yle_tf_plugins/commands/_config/plugin.rb +19 -0
- data/lib/yle_tf_plugins/commands/_shell/command.rb +15 -0
- data/lib/yle_tf_plugins/commands/_shell/plugin.rb +14 -0
- data/lib/yle_tf_plugins/commands/help/command.rb +55 -0
- data/lib/yle_tf_plugins/commands/help/plugin.rb +18 -0
- data/lib/yle_tf_plugins/commands/version/command.rb +20 -0
- data/lib/yle_tf_plugins/commands/version/plugin.rb +18 -0
- data/vendor/hash_deep_merge.rb +59 -0
- data/vendor/logger_level_patch.rb +29 -0
- data/vendor/middleware/LICENSE +23 -0
- data/vendor/middleware/builder.rb +149 -0
- data/vendor/middleware/runner.rb +69 -0
- data/yle_tf.gemspec +37 -0
- 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
|