yle_tf 1.0.0 → 1.4.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/bin/tf +1 -0
  3. data/lib/yle_tf.rb +4 -0
  4. data/lib/yle_tf/action.rb +4 -0
  5. data/lib/yle_tf/action/builder.rb +2 -0
  6. data/lib/yle_tf/action/command.rb +4 -2
  7. data/lib/yle_tf/action/copy_root_module.rb +2 -0
  8. data/lib/yle_tf/action/generate_vars_file.rb +2 -0
  9. data/lib/yle_tf/action/load_config.rb +9 -1
  10. data/lib/yle_tf/action/terraform_init.rb +63 -32
  11. data/lib/yle_tf/action/tf_hooks.rb +9 -5
  12. data/lib/yle_tf/action/tmpdir.rb +3 -1
  13. data/lib/yle_tf/action/verify_terraform_version.rb +19 -6
  14. data/lib/yle_tf/action/verify_tf_env.rb +2 -0
  15. data/lib/yle_tf/action/verify_yle_tf_version.rb +36 -0
  16. data/lib/yle_tf/action/write_terraformrc_defaults.rb +34 -17
  17. data/lib/yle_tf/backend.rb +47 -0
  18. data/lib/yle_tf/cli.rb +3 -1
  19. data/lib/yle_tf/config.rb +25 -9
  20. data/lib/yle_tf/config/defaults.rb +25 -11
  21. data/lib/yle_tf/config/erb.rb +2 -0
  22. data/lib/yle_tf/config/file.rb +2 -0
  23. data/lib/yle_tf/config/loader.rb +89 -59
  24. data/lib/yle_tf/config/migration.rb +117 -0
  25. data/lib/yle_tf/error.rb +2 -0
  26. data/lib/yle_tf/helpers/hash.rb +22 -0
  27. data/lib/yle_tf/logger.rb +2 -10
  28. data/lib/yle_tf/logger/colorize.rb +2 -0
  29. data/lib/yle_tf/plugin.rb +6 -2
  30. data/lib/yle_tf/plugin/action_hook.rb +2 -0
  31. data/lib/yle_tf/plugin/loader.rb +8 -1
  32. data/lib/yle_tf/plugin/manager.rb +3 -0
  33. data/lib/yle_tf/system.rb +5 -2
  34. data/lib/yle_tf/system/io_handlers.rb +5 -1
  35. data/lib/yle_tf/system/output_logger.rb +2 -0
  36. data/lib/yle_tf/system/tf_hook_output_logger.rb +2 -0
  37. data/lib/yle_tf/tf_hook.rb +11 -9
  38. data/lib/yle_tf/tf_hook/runner.rb +2 -0
  39. data/lib/yle_tf/vars_file.rb +16 -3
  40. data/lib/yle_tf/version.rb +3 -1
  41. data/lib/yle_tf/version_requirement.rb +2 -5
  42. data/lib/yle_tf_plugins/backends/{s3 → __default}/plugin.rb +6 -4
  43. data/lib/yle_tf_plugins/backends/file/backend.rb +89 -0
  44. data/lib/yle_tf_plugins/backends/file/plugin.rb +4 -2
  45. data/lib/yle_tf_plugins/commands/__default/command.rb +2 -0
  46. data/lib/yle_tf_plugins/commands/__default/plugin.rb +2 -0
  47. data/lib/yle_tf_plugins/commands/_config/command.rb +2 -0
  48. data/lib/yle_tf_plugins/commands/_config/plugin.rb +2 -0
  49. data/lib/yle_tf_plugins/commands/_shell/command.rb +2 -0
  50. data/lib/yle_tf_plugins/commands/_shell/plugin.rb +2 -0
  51. data/lib/yle_tf_plugins/commands/help/command.rb +9 -20
  52. data/lib/yle_tf_plugins/commands/help/plugin.rb +2 -0
  53. data/lib/yle_tf_plugins/commands/version/command.rb +2 -0
  54. data/lib/yle_tf_plugins/commands/version/plugin.rb +2 -0
  55. metadata +61 -22
  56. data/lib/yle_tf/backend_config.rb +0 -41
  57. data/lib/yle_tf_plugins/backends/file/command.rb +0 -31
  58. data/lib/yle_tf_plugins/backends/file/config.rb +0 -17
  59. data/lib/yle_tf_plugins/backends/s3/command.rb +0 -19
  60. data/lib/yle_tf_plugins/backends/swift/command.rb +0 -18
  61. data/lib/yle_tf_plugins/backends/swift/plugin.rb +0 -16
  62. data/vendor/logger_level_patch.rb +0 -29
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ class YleTf
6
+ class Backend
7
+ BACKEND_CONFIG_FILE = '_backend.tf.json'
8
+
9
+ attr_reader :config
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ end
14
+
15
+ def type
16
+ @type ||= config.fetch('backend', 'type')
17
+ end
18
+
19
+ def backend_specific_config
20
+ config.fetch('backend', type)
21
+ end
22
+
23
+ # Generate backend configuration file for Terraform
24
+ def configure
25
+ data = {
26
+ terraform: [{
27
+ backend: [to_h]
28
+ }]
29
+ }
30
+ File.write(BACKEND_CONFIG_FILE, JSON.pretty_generate(data))
31
+ end
32
+
33
+ # Tear down the backend
34
+ def tear_down
35
+ # Nothing to do by default
36
+ end
37
+
38
+ # Returns the backend configuration as a `Hash` for Terraform
39
+ def to_h
40
+ { type => backend_specific_config }
41
+ end
42
+
43
+ def to_s
44
+ to_h.to_s
45
+ end
46
+ end
47
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yle_tf'
2
4
 
3
5
  class YleTf
@@ -48,7 +50,7 @@ class YleTf
48
50
  if TF_OPTIONS.include?(arg)
49
51
  @tf_options[key(arg)] = true
50
52
  else
51
- STDERR.puts "Unknown option '#{arg}'"
53
+ warn "Unknown option '#{arg}'"
52
54
  @tf_command = 'help'
53
55
  @tf_env = 'error'
54
56
  break
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pathname'
2
4
  require 'yaml'
3
5
 
@@ -10,16 +12,23 @@ class YleTf
10
12
  class Config
11
13
  NotFoundError = Class.new(Error)
12
14
 
13
- attr_reader :config, :tf_env, :module_dir
15
+ # Loads the configuration based on the environment
16
+ def self.load(tf_env)
17
+ opts = {
18
+ tf_env: tf_env,
19
+ module_dir: Pathname.pwd
20
+ }
14
21
 
15
- def initialize(tf_env)
16
- Logger.debug("Initializing configuration for the #{tf_env.inspect} environment")
22
+ config = Loader.new(opts).load
23
+ new(config, **opts)
24
+ end
17
25
 
18
- @tf_env = tf_env
19
- @module_dir = Pathname.pwd
20
- @config = Loader.new(tf_env: tf_env, module_dir: module_dir).load
26
+ attr_reader :config, :tf_env, :module_dir
21
27
 
22
- Logger.debug(inspect)
28
+ def initialize(config, **opts)
29
+ @config = config
30
+ @tf_env = opts[:tf_env]
31
+ @module_dir = opts[:module_dir]
23
32
  end
24
33
 
25
34
  def to_s
@@ -33,8 +42,15 @@ class YleTf
33
42
  block ||= DEFAULT_NOT_FOUND_BLOCK
34
43
 
35
44
  keys.inject(config) do |conf, key|
36
- break block.call(keys) if !conf || !conf.key?(key)
37
- conf[key]
45
+ next conf[key] if conf.is_a?(Hash) && conf.key?(key)
46
+
47
+ if !conf.nil? && !conf.is_a?(Hash)
48
+ Logger.warn(
49
+ "Configuration [#{keys.join(' > ')}] includes non-hash element #{conf.inspect}"
50
+ )
51
+ end
52
+
53
+ break block.call(keys)
38
54
  end
39
55
  end
40
56
 
@@ -1,33 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yle_tf/helpers/hash'
4
+
1
5
  class YleTf
2
6
  class Config
3
7
  module Defaults
4
8
  DEFAULT_CONFIG = {
5
- 'hooks' => {
6
- 'pre' => [],
9
+ 'hooks' => {
10
+ 'pre' => [],
7
11
  'post' => []
8
12
  },
9
- 'backend' => {
13
+ 'backend' => {
10
14
  'type' => 'file',
11
- 'bucket' => nil,
12
- 'file' => '<%= @module %>_<%= @env %>.tfstate',
13
- 'region' => nil,
14
- 'encrypt' => false,
15
+ 'file' => {
16
+ 'path' => '<%= @module %>_<%= @env %>.tfstate',
17
+ 'encrypt' => false,
18
+ 'encrypt_command' => 'sops --encrypt --input-type binary --output-type binary --output "{{TO}}" "{{FROM}}"',
19
+ 'decrypt_command' => 'sops --decrypt --input-type binary --output-type binary --output "{{TO}}" "{{FROM}}"'
20
+ },
21
+ 's3' => {
22
+ 'key' => '<%= @module %>_<%= @env %>.tfstate'
23
+ }
15
24
  },
16
- 'tfvars' => {
25
+ 'tfvars' => {
17
26
  },
18
27
  'terraform' => {
19
28
  'version_requirement' => nil
29
+ },
30
+ 'yle_tf' => {
31
+ 'version_requirement' => nil
20
32
  }
21
33
  }.freeze
22
34
 
35
+ # Returns deep copy of the default config Hash.
23
36
  def default_config
24
- DEFAULT_CONFIG.dup
37
+ Helpers::Hash.deep_copy(DEFAULT_CONFIG)
25
38
  end
26
39
 
27
40
  def default_config_context
28
41
  {
29
- env: tf_env,
30
- module: module_dir.basename.to_s,
42
+ env: tf_env,
43
+ module: module_dir.basename.to_s,
44
+ module_dir: module_dir.to_s,
31
45
  }
32
46
  end
33
47
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'erb'
2
4
 
3
5
  class YleTf
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  require 'yle_tf/logger'
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yle_tf/config/defaults'
2
4
  require 'yle_tf/config/erb'
3
5
  require 'yle_tf/config/file'
6
+ require 'yle_tf/config/migration'
7
+ require 'yle_tf/helpers/hash'
4
8
  require 'yle_tf/logger'
5
9
  require 'yle_tf/plugin'
6
10
 
7
- require_relative '../../../vendor/hash_deep_merge'
8
-
9
11
  class YleTf
10
12
  class Config
11
13
  class Loader
@@ -19,20 +21,88 @@ class YleTf
19
21
  end
20
22
 
21
23
  def load
22
- Logger.debug('Loading default config')
23
- config = default_config
24
- Logger.debug(config.inspect)
24
+ load_sequence = %i[
25
+ load_default_config
26
+ load_plugin_configurations
27
+ load_config_files
28
+ evaluate_configuration_strings
29
+ ]
30
+ load_sequence.inject({}) { |config, method| send(method, config) }
31
+ end
32
+
33
+ def load_default_config(_config)
34
+ task('Loading default config') { default_config }
35
+ end
36
+
37
+ def load_plugin_configurations(config)
38
+ Logger.debug('Loading configuration from plugins')
39
+
40
+ plugins.inject(config) do |prev_config, plugin|
41
+ migrate_and_merge_configuration(prev_config, plugin.default_config,
42
+ type: 'plugin', name: plugin.to_s)
43
+ end
44
+ end
45
+
46
+ def load_config_files(config)
47
+ Logger.debug('Loading configuration from files')
48
+
49
+ config_files.inject(config) do |prev_config, file|
50
+ migrate_and_merge_configuration(prev_config, file.read,
51
+ type: 'file', name: file.name)
52
+ end
53
+ end
54
+
55
+ def evaluate_configuration_strings(config)
56
+ task('Evaluating the configuration strings') { eval_config(config) }
57
+ end
58
+
59
+ def eval_config(config)
60
+ case config
61
+ when Hash
62
+ config.each_with_object({}) { |(key, value), h| h[key] = eval_config(value) }
63
+ when Array
64
+ config.map { |item| eval_config(item) }
65
+ when String
66
+ Config::ERB.evaluate(config, config_context)
67
+ else
68
+ config
69
+ end
70
+ end
25
71
 
26
- Logger.debug('Merging default configurations from plugins')
27
- merge_plugin_configurations(config)
28
- Logger.debug(config.inspect)
72
+ def plugins
73
+ Plugin.manager.registered
74
+ end
75
+
76
+ def config_files
77
+ module_dir.descend.lazy
78
+ .map { |dir| dir.join('tf.yaml') }
79
+ .select(&:exist?)
80
+ .map { |file| Config::File.new(file) }
81
+ end
29
82
 
30
- Logger.debug('Merging configurations from files')
31
- merge_config_files(config)
32
- Logger.debug(config.inspect)
83
+ def migrate_and_merge_configuration(prev_config, config, **opts)
84
+ task("- #{opts[:name]}") { config }
85
+ return prev_config if config.empty?
33
86
 
34
- Logger.debug('Evaluating the configuration strings')
35
- eval_config(config)
87
+ source = "#{opts[:type]}: '#{opts[:name]}'"
88
+ config = migrate_old_config(config, source: source)
89
+ deep_merge(prev_config, config, source: source)
90
+ end
91
+
92
+ def migrate_old_config(config, **opts)
93
+ task(' -> Migrating') do
94
+ Config::Migration.migrate_old_config(config, **opts)
95
+ end
96
+ end
97
+
98
+ def deep_merge(prev_config, config, **opts)
99
+ task(' -> Merging') do
100
+ Helpers::Hash.deep_merge(prev_config, config)
101
+ end
102
+ rescue StandardError => e
103
+ Logger.fatal("Failed to merge configuration from #{opts[:source]}:\n" \
104
+ "#{config.inspect}\ninto:\n#{prev_config.inspect}")
105
+ raise e
36
106
  end
37
107
 
38
108
  def config_context
@@ -54,53 +124,13 @@ class YleTf
54
124
  end
55
125
  end
56
126
 
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
127
+ # Helper to print debug information about the task and configuration
128
+ # after it
129
+ def task(message = nil)
130
+ Logger.debug(message) if message
79
131
 
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
132
+ yield.tap do |config|
133
+ Logger.debug(" #{config.inspect}")
104
134
  end
105
135
  end
106
136
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'yle_tf/helpers/hash'
5
+ require 'yle_tf/logger'
6
+
7
+ class YleTf
8
+ class Config
9
+ class Migration
10
+ BACKEND_MIGRATIONS = {
11
+ 'file' => {
12
+ 'file' => 'path'
13
+ },
14
+ 's3' => {
15
+ 'region' => 'region',
16
+ 'bucket' => 'bucket',
17
+ 'file' => 'key',
18
+ 'encrypt' => 'encrypt'
19
+ },
20
+ 'swift' => {
21
+ 'region' => 'region_name',
22
+ 'container' => 'container',
23
+ 'archive_container' => 'archive_container'
24
+ }
25
+ }.freeze
26
+
27
+ include Helpers::Hash
28
+
29
+ def self.migrate_old_config(config, **opts)
30
+ new(config, **opts).migrated_config
31
+ end
32
+
33
+ attr_reader :config, :config_source
34
+
35
+ def initialize(config, **opts)
36
+ @config = config
37
+ @config_source = opts.fetch(:source)
38
+ end
39
+
40
+ def old_backend_config
41
+ config['backend']
42
+ end
43
+
44
+ def migrated_config
45
+ migrate_old_backend_config(&deprecation_warning)
46
+ end
47
+
48
+ # Returns a `Proc` to print deprecation warnings unless denied by an env var
49
+ def deprecation_warning
50
+ return nil if ENV['TF_OLD_CONFIG_WARNINGS'] == 'false'
51
+
52
+ lambda do |new_config|
53
+ Logger.warn("Old configuration found in #{config_source}")
54
+ Logger.warn("Please migrate to relevant parts of:\n" \
55
+ "#{sanitize_config(new_config)}")
56
+ Logger.warn(
57
+ 'See https://github.com/Yleisradio/yle_tf/wiki/Migrating-Configuration for more details'
58
+ )
59
+ end
60
+ end
61
+
62
+ # TODO: Remove support in v2.0
63
+ def migrate_old_backend_config
64
+ changed = false
65
+
66
+ new_config = BACKEND_MIGRATIONS.inject(config) do |prev_config, (type, keys)|
67
+ migrate_old_backend_config_keys(prev_config, type, keys) { changed = true }
68
+ end
69
+
70
+ yield(new_config) if changed && block_given?
71
+
72
+ new_config
73
+ end
74
+
75
+ def migrate_old_backend_config_keys(config, type, keys)
76
+ migrated_keys = find_old_backend_config_keys(keys)
77
+ return config if migrated_keys.empty?
78
+
79
+ defaults = {
80
+ 'backend' => {
81
+ type => {}
82
+ }
83
+ }
84
+ copy_with_defaults(config, defaults).tap do |new_config|
85
+ migrated_keys.each do |old_key, new_key|
86
+ new_config['backend'][type][new_key] = old_backend_config[old_key]
87
+ end
88
+
89
+ yield new_config
90
+ end
91
+ end
92
+
93
+ def find_old_backend_config_keys(keys)
94
+ return {} if !old_backend_config.is_a?(Hash)
95
+
96
+ keys.select do |old_key, _new_key|
97
+ old_backend_config.key?(old_key) &&
98
+ # Special case for 'file' as it is now used for option Hash for the
99
+ # 'file' backend
100
+ !(old_key == 'file' && old_backend_config['file'].is_a?(Hash))
101
+ end
102
+ end
103
+
104
+ def copy_with_defaults(config, defaults)
105
+ deep_merge(deep_copy(config), defaults)
106
+ end
107
+
108
+ def sanitize_config(config)
109
+ backend_config = config['backend'].select do |key, value|
110
+ key == 'type' || value.is_a?(Hash)
111
+ end
112
+
113
+ YAML.dump('backend' => backend_config)
114
+ end
115
+ end
116
+ end
117
+ end