yle_tf 1.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7811f980206094727502839932ba9a800a4e37694ae26bc71d9c4935de38d3c3
4
- data.tar.gz: 95e2c481a30b3caffb65c08407f9e222210fe01514f59ff88ca38a00915ea31b
3
+ metadata.gz: e8df78c6d65e3180770988e0fdc1655bfa0af42d57d8f1b5dd19f26de1281a39
4
+ data.tar.gz: 10f59a1cdd3757d19dbeebfaaeb1328e41c367be464d87486b9bb8b93d6ef845
5
5
  SHA512:
6
- metadata.gz: 84010f77dfa1d2786f0c9b4a3783073ad15bbf6146bac16f4a49401b8b378e28f0747f82a6880eb01dda175fccbd1465f3e25391586e10c11d424e304fe8e74b
7
- data.tar.gz: 95c19822da06edb58e250215fb0c5e2154da99e8881a2b8a8ce4fb9fe7c124772e81d7a2774595fdedb97cec3166e0b34e7c098ac20d8133ad97bfcb1196534e
6
+ metadata.gz: dc31eb920ce29b2aeb8616739b89e370a0c36a8ff395e3a41b3a1176d4ac592a2da913b6ff11eb0a0c015c33f5819cf5e888b9056b324e7ef81c156f75fa4fc6
7
+ data.tar.gz: 2fbcf63e27d8a817b654087d676a9252db1acde67629ad66a07e4d12ac20e44396c707718c42c3ee594593bcad74fb5d1be108026b5a17c615dc4663eca42a97
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
3
4
  require 'pathname'
5
+ require 'shellwords'
4
6
 
5
7
  require 'yle_tf/logger'
6
8
  require 'yle_tf/plugin'
@@ -28,26 +30,53 @@ class YleTf
28
30
  Logger.info('Initializing Terraform')
29
31
  Logger.debug("Backend configuration: #{backend}")
30
32
 
31
- init(backend)
33
+ init_dir
32
34
 
33
- @app.call(env)
35
+ if env[:tf_command] == 'init'
36
+ # Skip initializing Terraform here, as it will be done by the
37
+ # actuall command later in the middleware stack.
38
+ @app.call(env)
39
+ store_terraform_lock
40
+ else
41
+ init_terraform
42
+ store_terraform_lock
43
+ @app.call(env)
44
+ end
45
+
46
+ tear_down
34
47
  end
35
48
 
36
- def init(backend)
49
+ def init_dir
37
50
  Logger.debug('Configuring the backend')
38
51
  backend.configure
39
52
 
40
53
  Logger.debug('Symlinking errored.tfstate')
41
- symlink_errored_tfstate
54
+ symlink_to_module_dir('errored.tfstate')
55
+ end
42
56
 
57
+ def tear_down
58
+ Logger.debug('Tearing down backend')
59
+ backend.tear_down
60
+ end
61
+
62
+ def init_terraform
43
63
  Logger.debug('Initializing Terraform')
44
- YleTf::System.cmd('terraform', 'init', *TF_CMD_ARGS, **TF_CMD_OPTS)
64
+ YleTf::System.cmd('terraform', 'init', *tf_init_args, **TF_CMD_OPTS)
65
+ end
66
+
67
+ def store_terraform_lock
68
+ Logger.debug('Storing .terraform.lock.hcl')
69
+ copy_to_module_dir('.terraform.lock.hcl')
45
70
  end
46
71
 
47
72
  def backend
48
73
  @backend ||= find_backend
49
74
  end
50
75
 
76
+ def tf_init_args
77
+ TF_CMD_ARGS + Shellwords.split(ENV.fetch('TF_INIT_ARGS', ''))
78
+ end
79
+
51
80
  def find_backend
52
81
  backend_type = config.fetch('backend', 'type').downcase
53
82
  backend_proc = Plugin.manager.backends[backend_type]
@@ -56,15 +85,19 @@ class YleTf
56
85
  klass.new(config)
57
86
  end
58
87
 
59
- def symlink_errored_tfstate
60
- local_path = Pathname.pwd.join('errored.tfstate')
61
- remote_path = config.module_dir.join('errored.tfstate')
88
+ def symlink_to_module_dir(file)
89
+ local_path = Pathname.pwd.join(file)
90
+ remote_path = config.module_dir.join(file)
62
91
 
63
92
  # Remove the possibly copied old file
64
93
  local_path.unlink if local_path.exist?
65
94
 
66
95
  local_path.make_symlink(remote_path)
67
96
  end
97
+
98
+ def copy_to_module_dir(file)
99
+ FileUtils.cp(file, config.module_dir.to_s) if File.exist?(file)
100
+ end
68
101
  end
69
102
  end
70
103
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
+ require 'pathname'
5
+
4
6
  require 'yle_tf/logger'
5
7
 
6
8
  class YleTf
@@ -10,45 +12,58 @@ class YleTf
10
12
  RC_PATH = '~/.terraformrc'
11
13
 
12
14
  # Path of the plugin cache directory
13
- DEFAULT_PLUGIN_CACHE_PATH = '$HOME/.terraform.d/plugin-cache'
15
+ DEFAULT_PLUGIN_CACHE_PATH = '~/.terraform.d/plugin-cache'
14
16
 
15
17
  def initialize(app)
16
18
  @app = app
17
19
  end
18
20
 
19
21
  def call(env)
20
- Logger.debug("Writing default configuration to '#{RC_PATH}'")
21
- open_rc_file do |rc_file|
22
- keys = existing_keys(rc_file)
23
-
24
- configure_checkpoint(rc_file) if !keys.include?('disable_checkpoint')
25
- configure_plugin_cache_dir(rc_file) if !keys.include?('plugin_cache_dir')
22
+ if rc_file.exist?
23
+ Logger.debug("Terraform configuration file '#{RC_PATH}' already exists")
24
+ if !existing_keys.include?('plugin_cache_dir')
25
+ Logger.warn("'plugin_cache_dir' not configured in '#{RC_PATH}'")
26
+ end
27
+ else
28
+ Logger.debug("Writing default configuration to '#{RC_PATH}'")
29
+ write_default_config
26
30
  end
27
31
 
28
32
  @app.call(env)
29
33
  end
30
34
 
31
- def open_rc_file(&block)
32
- File.open(File.expand_path(RC_PATH), 'a+', &block)
35
+ def rc_file
36
+ @rc_file ||= Pathname.new(RC_PATH).expand_path
33
37
  end
34
38
 
35
- def existing_keys(rc_file)
36
- [].tap do |keys|
37
- rc_file.each_line do |line|
39
+ def existing_keys
40
+ @existing_keys ||= [].tap do |keys|
41
+ rc_file.readlines.each do |line|
42
+ # The matcher is a bit naive, but enough for out use
38
43
  keys << Regexp.last_match(1) if line =~ /^(.+?)[ \t]*=/
39
44
  end
40
45
  end
41
46
  end
42
47
 
43
- def configure_checkpoint(rc_file)
48
+ def write_default_config
49
+ rc_file.open('w') do |rc_file|
50
+ configure_checkpoint(rc_file)
51
+ configure_plugin_cache_dir(rc_file)
52
+ end
53
+ end
54
+
55
+ def configure_checkpoint(file)
44
56
  Logger.info("Disabling Terraform upgrade and security bulletin checks by '#{RC_PATH}'")
45
57
 
46
- rc_file.puts('disable_checkpoint = true')
58
+ file.puts('disable_checkpoint = true')
47
59
  end
48
60
 
49
- def configure_plugin_cache_dir(rc_file)
61
+ def configure_plugin_cache_dir(file)
50
62
  Logger.info("Configuring global Terraform plugin cache by '#{RC_PATH}'")
51
- rc_file.puts("plugin_cache_dir = \"#{DEFAULT_PLUGIN_CACHE_PATH}\"")
63
+ # Replace `~` with `$HOME` as it is not expanded correctly in all architectures.
64
+ # Can't use `$HOME` in the constant though, as it won't be expanded by
65
+ # `expand_path` below. Can't win this game.
66
+ file.puts("plugin_cache_dir = \"#{DEFAULT_PLUGIN_CACHE_PATH.sub(/^~/, '$HOME')}\"")
52
67
 
53
68
  dir = File.expand_path(DEFAULT_PLUGIN_CACHE_PATH)
54
69
  return if File.directory?(dir)
@@ -30,6 +30,11 @@ class YleTf
30
30
  File.write(BACKEND_CONFIG_FILE, JSON.pretty_generate(data))
31
31
  end
32
32
 
33
+ # Tear down the backend
34
+ def tear_down
35
+ # Nothing to do by default
36
+ end
37
+
33
38
  # Returns the backend configuration as a `Hash` for Terraform
34
39
  def to_h
35
40
  { type => backend_specific_config }
@@ -13,7 +13,10 @@ class YleTf
13
13
  'backend' => {
14
14
  'type' => 'file',
15
15
  'file' => {
16
- 'path' => '<%= @module %>_<%= @env %>.tfstate'
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}}"'
17
20
  },
18
21
  's3' => {
19
22
  'key' => '<%= @module %>_<%= @env %>.tfstate'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class YleTf
4
- VERSION = '1.3.0'
4
+ VERSION = '1.4.0'
5
5
  end
@@ -1,24 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
3
4
  require 'pathname'
5
+ require 'shellwords'
6
+
4
7
  require 'yle_tf/backend'
5
8
  require 'yle_tf/logger'
9
+ require 'yle_tf/system'
6
10
 
7
11
  module YleTfPlugins
8
12
  module Backends
9
13
  module File
10
14
  class Backend < YleTf::Backend
11
- # Symlinks local "terraform.tfstate" to the specified path
12
15
  def configure
16
+ if !encrypt?
17
+ create_tfstate(tfstate_path)
18
+ symlink_tfstate
19
+ elsif tfstate_path.exist?
20
+ decrypt_tfstate
21
+ end
22
+ end
23
+
24
+ def tear_down
25
+ encrypt_tfstate if encrypt? && local_tfstate_path.exist?
26
+ end
27
+
28
+ def create_tfstate(path)
29
+ return if path.exist?
30
+
31
+ YleTf::Logger.debug('Creating state file')
32
+ path.write(tfstate_template, perm: 0o644)
33
+ end
34
+
35
+ def symlink_tfstate
13
36
  YleTf::Logger.info("Symlinking state to '#{tfstate_path}'")
14
- local_path = Pathname.pwd.join('terraform.tfstate')
15
- local_path.make_symlink(tfstate_path)
37
+ local_tfstate_path.make_symlink(tfstate_path)
38
+ end
16
39
 
17
- tfstate_path.write(tfstate_template, perm: 0o644) if !tfstate_path.exist?
40
+ def encrypt?
41
+ config.fetch('backend', type, 'encrypt')
42
+ end
43
+
44
+ def decrypt_tfstate
45
+ YleTf::Logger.info("Decrypting state from '#{tfstate_path}'")
46
+
47
+ cmd = config.fetch('backend', type, 'decrypt_command')
48
+ cmd.gsub!('{{FROM}}', tfstate_path.to_s)
49
+ cmd.gsub!('{{TO}}', local_tfstate_path.to_s)
50
+
51
+ # Split the command to have nicer logs
52
+ YleTf::System.cmd(*Shellwords.split(cmd))
53
+ end
54
+
55
+ def encrypt_tfstate
56
+ YleTf::Logger.info("Encrypting state to '#{tfstate_path}'")
57
+
58
+ cmd = config.fetch('backend', type, 'encrypt_command')
59
+ cmd.gsub!('{{FROM}}', local_tfstate_path.to_s)
60
+ cmd.gsub!('{{TO}}', tfstate_path.to_s)
61
+
62
+ YleTf::System.cmd(*Shellwords.split(cmd),
63
+ error_handler: method(:on_encrypt_error))
64
+ end
65
+
66
+ def on_encrypt_error(_exit_code, error)
67
+ plain_tfstate_path = "#{tfstate_path}.plaintext"
68
+
69
+ YleTf::Logger.warn("Copying unencrypted state to '#{plain_tfstate_path}'")
70
+ FileUtils.cp(local_tfstate_path.to_s, plain_tfstate_path)
71
+
72
+ raise error
18
73
  end
19
74
 
20
75
  def tfstate_path
21
- @tfstate_path ||= config.module_dir.join(config.fetch('backend', 'file', 'path'))
76
+ @tfstate_path ||= config.module_dir.join(config.fetch('backend', type, 'path'))
77
+ end
78
+
79
+ def local_tfstate_path
80
+ @local_tfstate_path ||= Pathname.pwd.join('terraform.tfstate')
22
81
  end
23
82
 
24
83
  def tfstate_template
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'optparse'
4
4
 
5
- require 'yle_tf/system'
6
5
  require 'yle_tf/plugin'
7
6
 
8
7
  module YleTfPlugins
@@ -20,18 +19,17 @@ module YleTfPlugins
20
19
  o.banner = 'Usage: tf <environment> <command> [<args>]'
21
20
  o.separator ''
22
21
  o.separator 'YleTf options:'
23
- o.on('-h', '--help', 'Prints this help')
22
+ o.on('-h', '--help', 'Prints this help')
24
23
  o.on('-v', '--version', 'Prints the version information')
25
- o.on('--debug', 'Print debug information')
26
- o.on('--no-color', 'Do not output with colors')
27
- o.on('--no-hooks', 'Do not run any hooks')
28
- o.on('--only-hooks', 'Only run the hooks')
24
+ o.on('--debug', 'Print debug information')
25
+ o.on('--no-color', 'Do not output with colors')
26
+ o.on('--no-hooks', 'Do not run any hooks')
27
+ o.on('--only-hooks', 'Only run the hooks')
29
28
  o.separator ''
30
- o.separator 'Special tf commands:'
29
+ o.separator 'Special YleTf commands:'
31
30
  o.separator tf_command_help
32
31
  o.separator ''
33
- o.separator 'Terraform commands:'
34
- o.separator terraform_help
32
+ o.separator 'Run `terraform -help` to get list of all Terraform commands.'
35
33
  end
36
34
  end
37
35
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
@@ -49,17 +47,6 @@ module YleTfPlugins
49
47
  " #{command.ljust(18)} #{data[:synopsis]}"
50
48
  end
51
49
  end
52
-
53
- def terraform_help
54
- on_error = proc do |exit_code|
55
- # exit_code is nil if the command was not found
56
- # Ignore other exit codes, as older Terraform versions return
57
- # non-zero exit codes for the help.
58
- return ' [Terraform not found]' if exit_code.nil?
59
- end
60
- help = YleTf::System.read_cmd('terraform', '--help', error_handler: on_error)
61
- help.lines.grep(/^ /)
62
- end
63
50
  end
64
51
  end
65
52
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yle_tf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yleisradio
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-10-12 00:00:00.000000000 Z
13
+ date: 2021-01-27 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: thwait
@@ -182,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
182
182
  - !ruby/object:Gem::Version
183
183
  version: '0'
184
184
  requirements: []
185
- rubygems_version: 3.1.2
185
+ rubygems_version: 3.2.3
186
186
  signing_key:
187
187
  specification_version: 4
188
188
  summary: Tooling for Terraform