yle_tf 1.3.0 → 1.4.0

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