yle_tf 1.0.0 → 1.4.0

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