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