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,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
@@ -0,0 +1,4 @@
1
+ class YleTf
2
+ # Base class for yle_tf errors
3
+ Error = Class.new(StandardError)
4
+ end