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,41 @@
|
|
1
|
+
require 'yle_tf/error'
|
2
|
+
require 'yle_tf/logger'
|
3
|
+
require 'yle_tf/version_requirement'
|
4
|
+
|
5
|
+
class YleTf
|
6
|
+
module Action
|
7
|
+
class VerifyTerraformVersion
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
Logger.debug('Verifying Terraform version')
|
14
|
+
|
15
|
+
version = env[:terraform_version] = terraform_version
|
16
|
+
raise(Error, 'Terraform not found') if !version
|
17
|
+
|
18
|
+
Logger.debug("Terraform version: #{version}")
|
19
|
+
verify_version(env)
|
20
|
+
|
21
|
+
@app.call(env)
|
22
|
+
end
|
23
|
+
|
24
|
+
def terraform_version
|
25
|
+
# TODO: move `command` to YleTf::System
|
26
|
+
Regexp.last_match(1) if `terraform version` =~ /^Terraform v([^\s]+)/
|
27
|
+
rescue Errno::ENOENT
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def verify_version(env)
|
32
|
+
version = env[:terraform_version]
|
33
|
+
requirement = env[:config].fetch('terraform', 'version_requirement') { nil }
|
34
|
+
|
35
|
+
if !VersionRequirement.new(requirement).satisfied_by?(version)
|
36
|
+
raise Error, "Terraform version '#{requirement}' required, '#{version}' found"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'yle_tf/error'
|
2
|
+
require 'yle_tf/vars_file'
|
3
|
+
|
4
|
+
class YleTf
|
5
|
+
module Action
|
6
|
+
class VerifyTfEnv
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
config = env[:config]
|
13
|
+
all_envs = VarsFile.list_all_envs(config)
|
14
|
+
|
15
|
+
if !all_envs.include?(config.tf_env)
|
16
|
+
raise Error, "Terraform vars file not found for the '#{config.tf_env}' " \
|
17
|
+
" environment. Existing envs: #{all_envs.join(', ')}"
|
18
|
+
end
|
19
|
+
|
20
|
+
@app.call(env)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class YleTf
|
4
|
+
class BackendConfig
|
5
|
+
BACKEND_CONFIG_FILE = '_backend.tf.json'.freeze
|
6
|
+
|
7
|
+
attr_reader :type, :config
|
8
|
+
|
9
|
+
def initialize(type, config)
|
10
|
+
@type = type
|
11
|
+
@config = config
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns an `Array` of CLI args for Terraform pre 0.9 `init` command
|
15
|
+
def cli_args
|
16
|
+
args = ["-backend=#{type}"]
|
17
|
+
config.each do |key, value|
|
18
|
+
args << "-backend-config=#{key}=#{value}"
|
19
|
+
end
|
20
|
+
args
|
21
|
+
end
|
22
|
+
|
23
|
+
# Generate backend configuration file for Terraform v0.9+
|
24
|
+
def generate_config
|
25
|
+
data = {
|
26
|
+
terraform: [{
|
27
|
+
backend: [to_h]
|
28
|
+
}]
|
29
|
+
}
|
30
|
+
File.write(BACKEND_CONFIG_FILE, JSON.pretty_generate(data))
|
31
|
+
yield if block_given?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the backend configuration as a `Hash` for Terraform v0.9+
|
35
|
+
def to_h
|
36
|
+
{ type => config }
|
37
|
+
end
|
38
|
+
|
39
|
+
alias to_s to_h
|
40
|
+
end
|
41
|
+
end
|
data/lib/yle_tf/cli.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'yle_tf'
|
2
|
+
|
3
|
+
class YleTf
|
4
|
+
class CLI
|
5
|
+
attr_reader :tf_options, :tf_command, :tf_command_args, :tf_env
|
6
|
+
|
7
|
+
# YleTf option arguments
|
8
|
+
TF_OPTIONS = %w[--debug --no-hooks --only-hooks].freeze
|
9
|
+
|
10
|
+
HELP_ARGS = %w[-h --help help].freeze
|
11
|
+
VERSION_ARGS = %w[-v --version version].freeze
|
12
|
+
|
13
|
+
def initialize(argv)
|
14
|
+
@tf_options = {}
|
15
|
+
@tf_command_args = []
|
16
|
+
split_args(argv)
|
17
|
+
end
|
18
|
+
|
19
|
+
def execute
|
20
|
+
tf = YleTf.new(tf_options, tf_env, tf_command, tf_command_args)
|
21
|
+
tf.run
|
22
|
+
rescue YleTf::Error => e
|
23
|
+
raise e if debug?
|
24
|
+
|
25
|
+
Logger.fatal e
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
|
29
|
+
# rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/MethodLength
|
30
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
31
|
+
def split_args(argv)
|
32
|
+
argv.each do |arg|
|
33
|
+
if @tf_env && @tf_command
|
34
|
+
if TF_OPTIONS.include?(arg)
|
35
|
+
@tf_options[key(arg)] = true
|
36
|
+
else
|
37
|
+
@tf_command_args << arg
|
38
|
+
end
|
39
|
+
elsif HELP_ARGS.include?(arg)
|
40
|
+
@tf_command = 'help'
|
41
|
+
@tf_env = '_'
|
42
|
+
break
|
43
|
+
elsif VERSION_ARGS.include?(arg)
|
44
|
+
@tf_command = 'version'
|
45
|
+
@tf_env = '_'
|
46
|
+
break
|
47
|
+
elsif arg.start_with?('-')
|
48
|
+
if TF_OPTIONS.include?(arg)
|
49
|
+
@tf_options[key(arg)] = true
|
50
|
+
else
|
51
|
+
STDERR.puts "Unknown option '#{arg}'"
|
52
|
+
@tf_command = 'help'
|
53
|
+
@tf_env = 'error'
|
54
|
+
break
|
55
|
+
end
|
56
|
+
elsif !@tf_env
|
57
|
+
@tf_env = arg
|
58
|
+
else
|
59
|
+
@tf_command = arg
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
if !@tf_command || !@tf_env
|
64
|
+
@tf_command = 'help'
|
65
|
+
@tf_env = 'error'
|
66
|
+
end
|
67
|
+
|
68
|
+
self.debug = true if @tf_options.include?(:debug)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns `Symbol` for the arg, e.g. `"--foo-bar"` -> `:foo_bar`
|
72
|
+
def key(arg)
|
73
|
+
arg.sub(/\A--?/, '').tr('-', '_').to_sym
|
74
|
+
end
|
75
|
+
|
76
|
+
def debug=(value)
|
77
|
+
if value
|
78
|
+
ENV['TF_DEBUG'] = '1'
|
79
|
+
else
|
80
|
+
ENV.delete('TF_DEBUG')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def debug?
|
85
|
+
ENV.key?('TF_DEBUG')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'yle_tf/config/loader'
|
5
|
+
require 'yle_tf/error'
|
6
|
+
require 'yle_tf/logger'
|
7
|
+
|
8
|
+
class YleTf
|
9
|
+
# Configuration object to be used especially by the middleware stack
|
10
|
+
class Config
|
11
|
+
NotFoundError = Class.new(Error)
|
12
|
+
|
13
|
+
attr_reader :config, :tf_env, :module_dir
|
14
|
+
|
15
|
+
def initialize(tf_env)
|
16
|
+
Logger.debug("Initializing configuration for the #{tf_env.inspect} environment")
|
17
|
+
|
18
|
+
@tf_env = tf_env
|
19
|
+
@module_dir = Pathname.pwd
|
20
|
+
@config = Loader.new(tf_env: tf_env, module_dir: module_dir).load
|
21
|
+
|
22
|
+
Logger.debug(inspect)
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
YAML.dump(config)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns a value from the configuration hierarchy specified by a list of
|
30
|
+
# keys. If the key is not specified, return result of a specied block, or
|
31
|
+
# raise `NotFoundError` if none specified.
|
32
|
+
def fetch(*keys, &block)
|
33
|
+
block ||= DEFAULT_NOT_FOUND_BLOCK
|
34
|
+
|
35
|
+
keys.inject(config) do |conf, key|
|
36
|
+
break block.call(keys) if !conf || !conf.key?(key)
|
37
|
+
conf[key]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
DEFAULT_NOT_FOUND_BLOCK = lambda do |keys|
|
42
|
+
raise NotFoundError, "Configuration key not found: #{keys.join(' > ')}"
|
43
|
+
end.freeze
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class YleTf
|
2
|
+
class Config
|
3
|
+
module Defaults
|
4
|
+
DEFAULT_CONFIG = {
|
5
|
+
'hooks' => {
|
6
|
+
'pre' => [],
|
7
|
+
'post' => []
|
8
|
+
},
|
9
|
+
'backend' => {
|
10
|
+
'type' => 'file',
|
11
|
+
'bucket' => nil,
|
12
|
+
'file' => '<%= @module %>_<%= @env %>.tfstate',
|
13
|
+
'region' => nil,
|
14
|
+
'encrypt' => false,
|
15
|
+
},
|
16
|
+
'tfvars' => {
|
17
|
+
},
|
18
|
+
'terraform' => {
|
19
|
+
'version_requirement' => nil
|
20
|
+
}
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
def default_config
|
24
|
+
DEFAULT_CONFIG.dup
|
25
|
+
end
|
26
|
+
|
27
|
+
def default_config_context
|
28
|
+
{
|
29
|
+
env: tf_env,
|
30
|
+
module: module_dir.basename.to_s,
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
class YleTf
|
4
|
+
class Config
|
5
|
+
module ERB
|
6
|
+
class Context
|
7
|
+
def initialize(vars)
|
8
|
+
vars.each { |key, value| instance_variable_set(:"@#{key}", value) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def binding
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.evaluate(string, vars = {})
|
17
|
+
b = Context.new(vars).binding
|
18
|
+
::ERB.new(string).result(b)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
require 'yle_tf/logger'
|
4
|
+
|
5
|
+
class YleTf
|
6
|
+
class Config
|
7
|
+
class File
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@name = name.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def read
|
15
|
+
YAML.load_file(name) || {}
|
16
|
+
rescue StandardError => e
|
17
|
+
Logger.fatal("Failed to load or parse configuration from '#{name}'")
|
18
|
+
raise e
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'yle_tf/config/defaults'
|
2
|
+
require 'yle_tf/config/erb'
|
3
|
+
require 'yle_tf/config/file'
|
4
|
+
require 'yle_tf/logger'
|
5
|
+
require 'yle_tf/plugin'
|
6
|
+
|
7
|
+
require_relative '../../../vendor/hash_deep_merge'
|
8
|
+
|
9
|
+
class YleTf
|
10
|
+
class Config
|
11
|
+
class Loader
|
12
|
+
include Config::Defaults
|
13
|
+
|
14
|
+
attr_reader :tf_env, :module_dir
|
15
|
+
|
16
|
+
def initialize(opts)
|
17
|
+
@tf_env = opts.fetch(:tf_env)
|
18
|
+
@module_dir = opts.fetch(:module_dir)
|
19
|
+
end
|
20
|
+
|
21
|
+
def load
|
22
|
+
Logger.debug('Loading default config')
|
23
|
+
config = default_config
|
24
|
+
Logger.debug(config.inspect)
|
25
|
+
|
26
|
+
Logger.debug('Merging default configurations from plugins')
|
27
|
+
merge_plugin_configurations(config)
|
28
|
+
Logger.debug(config.inspect)
|
29
|
+
|
30
|
+
Logger.debug('Merging configurations from files')
|
31
|
+
merge_config_files(config)
|
32
|
+
Logger.debug(config.inspect)
|
33
|
+
|
34
|
+
Logger.debug('Evaluating the configuration strings')
|
35
|
+
eval_config(config)
|
36
|
+
end
|
37
|
+
|
38
|
+
def config_context
|
39
|
+
@config_context ||= load_config_context
|
40
|
+
end
|
41
|
+
|
42
|
+
def load_config_context
|
43
|
+
Logger.debug('Loading config context')
|
44
|
+
default_config_context.tap do |context|
|
45
|
+
Logger.debug('Merging configuration contexts from plugins')
|
46
|
+
merge_plugin_config_contexts(context)
|
47
|
+
Logger.debug("config_context: #{context.inspect}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def merge_plugin_config_contexts(context)
|
52
|
+
Plugin.manager.config_contexts.each do |plugin_context|
|
53
|
+
context.merge!(plugin_context)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def merge_plugin_configurations(config)
|
58
|
+
Plugin.manager.default_configs.each do |plugin_config|
|
59
|
+
deep_merge(
|
60
|
+
config, plugin_config,
|
61
|
+
error_msg:
|
62
|
+
"Failed to merge a plugin's default configuration:\n" \
|
63
|
+
"#{plugin_config.inspect}\ninto:\n#{config.inspect}"
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def merge_config_files(config)
|
69
|
+
config_files do |file|
|
70
|
+
Logger.debug(" - #{file}")
|
71
|
+
deep_merge(
|
72
|
+
config, file.read,
|
73
|
+
error_msg:
|
74
|
+
"Failed to merge configuration from '#{file}' into:\n" \
|
75
|
+
"#{config.inspect}"
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def deep_merge(config, new_config, opts = {})
|
81
|
+
config.deep_merge!(new_config)
|
82
|
+
rescue StandardError => e
|
83
|
+
Logger.fatal(opts[:error_msg]) if opts[:error_msg]
|
84
|
+
raise e
|
85
|
+
end
|
86
|
+
|
87
|
+
def config_files
|
88
|
+
module_dir.descend do |dir|
|
89
|
+
file = dir.join('tf.yaml')
|
90
|
+
yield(Config::File.new(file)) if file.exist?
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def eval_config(config)
|
95
|
+
case config
|
96
|
+
when Hash
|
97
|
+
config.each_with_object({}) { |(key, value), h| h[key] = eval_config(value) }
|
98
|
+
when Array
|
99
|
+
config.map { |item| eval_config(item) }
|
100
|
+
when String
|
101
|
+
Config::ERB.evaluate(config, config_context)
|
102
|
+
else
|
103
|
+
config
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/yle_tf/error.rb
ADDED