ndr_dev_support 7.3.0 → 7.3.2

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: 41d7ba2652d9fa563d40304d74636a120fabce678f2dac3247be956ab448e477
4
- data.tar.gz: 52be1d6408975a0777733c5ab9007098ffe2128360b00f13857c591ac051a0db
3
+ metadata.gz: bb40a54108485d9f25a6cc88323fb474f6fd1b7ba7af787939dc8270bf7ceaf2
4
+ data.tar.gz: 8120053e06f2443829e917fda979987e9b09e8c568e75781876b338dbfa53ebb
5
5
  SHA512:
6
- metadata.gz: d83ee219968cfbffb0f3b2eb9615442c595d6013ba1e0c21f04421197cc68dd94df9338e152922b1e39a566dd4ce7a35543c9672eb6b21548f3f80688b82610a
7
- data.tar.gz: fc6eb48554bd132c6bc9bf852ca5d708baaae03c61e36a0dfd8880665d3859418d3ba28a179939a801ba9262c956c41b5db06e5c8f11fd904da7870499c5138f
6
+ metadata.gz: 8685d5ff16b7c3ba001490bc5897647266bef21cfa94b80eaff77678953052afa9b04c4cbedd2fa5ae7652f7fcd5d08ef0f15f12107527608a5d5c601cfdcb9c
7
+ data.tar.gz: 7c9c95bb049feccf7516c49bc4e2240d43e5aadc8596cbda484163e5fa25f8e4902fd8176c38b9f1be2bee1b3776307701eb369fa0b4b0f0dfdeb189fb1cf0d5
data/CHANGELOG.md CHANGED
@@ -1,6 +1,21 @@
1
1
  ## [Unreleased]
2
2
  *no unreleased changes*
3
3
 
4
+ ## 7.3.2 / 2025-05-01
5
+ ### Fixed
6
+ * Capistrano: Add missing `tmpdir` requirement to deploy application secrets
7
+ * Capistrano: cap deploy:setup should be safe on existing deployments
8
+
9
+ ### Added
10
+ * Capistrano: add task deploy:preinstall to preinstall ruby and bundled gems
11
+
12
+ ## Changed
13
+ * Drop support for Rails 6.1
14
+
15
+ ## 7.3.1 / 2025-01-02
16
+ ### Added
17
+ * Capistrano: deploy application secrets from a subversion or git repository
18
+
4
19
  ## 7.3.0 / 2024-12-19
5
20
  ### Added
6
21
  * Capistrano: install rbenv and ruby from /opt/rbenv.tar.gz or vendor/rbenv/
@@ -4,7 +4,7 @@
4
4
  #
5
5
  # See the README for instructions on using in a project.
6
6
 
7
- require:
7
+ plugins:
8
8
  - rubocop-rails
9
9
  - rubocop-rake
10
10
 
@@ -0,0 +1,168 @@
1
+ require 'tmpdir'
2
+
3
+ # Add a git or svn secrets respository for ndr_dev_support:deploy_secrets
4
+ def add_secrets_repo(name:, url:, scm:, branch: nil)
5
+ raise "Invalid repo name #{name}" unless /\A[A-Z0-9_-]+\z/i.match?(name)
6
+ raise "Unknown scm #{scm}" unless %w[svn git].include?(scm)
7
+ raise "Expected branch for repo #{name}" if scm == 'git' && branch.to_s.empty?
8
+
9
+ secrets_repositories = fetch(:secrets_repositories, {})
10
+ secrets_repositories[name] = { url: url, scm: scm, branch: branch }
11
+ set :secrets_repositories, secrets_repositories
12
+ end
13
+
14
+ # Add a secret to be deployed by ndr_dev_support:deploy_secrets
15
+ def add_secret(repo:, repo_path:, shared_dest:)
16
+ secrets = fetch(:secrets, [])
17
+ raise "Unknown repo #{repo}" unless fetch(:secrets_repositories, {}).key?(repo)
18
+
19
+ secrets << { repo: repo, repo_path: repo_path, shared_dest: shared_dest }
20
+ set :secrets, secrets
21
+ end
22
+
23
+ Capistrano::Configuration.instance(:must_exist).load do
24
+ namespace :ndr_dev_support do
25
+ desc <<~DESC
26
+ Deploy updated application secrets to shared folders on application servers
27
+
28
+ To use this in a project, add something like the code below to your
29
+ Capistrano file config/deploy.rb, then run:
30
+ $ cap target app:update_secrets
31
+
32
+ namespace :app do
33
+ desc 'Update application secrets'
34
+ task :update_secrets do
35
+ add_secrets_repo(name: 'userlists',
36
+ url: 'https://github.com/example/users.git',
37
+ branch: 'main',
38
+ scm: 'git')
39
+ add_secrets_repo(name: 'encrypted_credentials_store',
40
+ url: 'https://svn-server.example.org/svn/creds', scm: 'svn')
41
+
42
+ add_secret(repo: 'encrypted_credentials_store',
43
+ repo_path: 'path/to/credentials.yml.enc',
44
+ shared_dest: 'config/credentials.yml.enc')
45
+ add_secret(repo: 'userlists',
46
+ repo_path: 'config/userlist.yml',
47
+ shared_dest: 'config/userlist.yml')
48
+ end
49
+ end
50
+ after 'app:update_secrets', 'ndr_dev_support:deploy_secrets'
51
+ DESC
52
+ task :deploy_secrets do
53
+ # List of repositories used for secrets
54
+ secrets_repositories = fetch(:secrets_repositories, {})
55
+ secrets = fetch(:secrets, [])
56
+ secrets_repo_base = Pathname.new('tmp/deployment-secrets')
57
+
58
+ if secrets.empty?
59
+ Capistrano::CLI.ui.say 'Warning: No secret files configured to upload'
60
+ next
61
+ end
62
+
63
+ # Allow quick indexing by filename
64
+ secrets_map = secrets.to_h { |secret| [secret[:shared_dest], secret] } # rubocop:disable Rails/IndexBy
65
+ changed = [] # List of changed files updated
66
+ Dir.mktmpdir do |secret_dir|
67
+ # Clone git secrets repositories if required
68
+ used_repos = secrets.collect { |secret| secret[:repo] }.uniq
69
+ repo_dirs = {}
70
+ used_repos.each do |repo|
71
+ repository = secrets_repositories[repo]
72
+ next unless repository[:scm] == 'git'
73
+
74
+ repo_dir = Pathname.new(secrets_repo_base).join(".git-#{repo}").to_s
75
+ if File.directory?(repo_dir)
76
+ ok = system("cd #{Shellwords.escape(repo_dir)} && git fetch")
77
+ raise "Error: cannot fetch secrets repository #{repo}: aborting" unless ok
78
+ else
79
+ ok = system('git', 'clone', '--mirror', '--filter=blob:none', repository[:url], repo_dir)
80
+ raise "Error: cannot clone secrets repository #{repo}: aborting" unless ok
81
+ end
82
+ repo_dirs[repo] = repo_dir
83
+ end
84
+
85
+ # Set up a temporary secrets directory of exported secrets,
86
+ # creating nested structure if necessary
87
+ secrets_map.each_value do |secret|
88
+ repo = secret[:repo]
89
+ repository = secrets_repositories[repo]
90
+ raise "Unknown repository #{secret[:repo]}" unless repository
91
+
92
+ repo_root = repository[:url]
93
+ raise 'Unknown / unsupported repository' unless repo_root&.start_with?('https://')
94
+
95
+ dest_fname = File.join(secret_dir, secret[:shared_dest])
96
+ dest_dir = File.dirname(dest_fname)
97
+ FileUtils.mkdir_p(dest_dir)
98
+ case repository[:scm]
99
+ when 'git'
100
+ repo_dir = Pathname.new(secrets_repo_base).join(".git-#{repo}").to_s
101
+ ok = system("GIT_DIR=#{Shellwords.escape(repo_dir)} git archive --format=tar " \
102
+ "#{Shellwords.escape(repository[:branch])} " \
103
+ "#{Shellwords.escape(secret[:repo_path])} | " \
104
+ "tar x -Ps %#{Shellwords.escape(secret[:repo_path])}%" \
105
+ "#{Shellwords.escape(File.join(secret_dir, secret[:shared_dest]))}% " \
106
+ "#{Shellwords.escape(secret[:repo_path])}")
107
+ when 'svn'
108
+ ok = system('svn', 'export', '--quiet', "#{repo_root}/#{secret[:repo_path]}",
109
+ File.join(secret_dir, secret[:shared_dest]))
110
+ # TODO: use --non-interactive, and then run again interactively if there's an eror
111
+ else
112
+ raise "Error: unsupported scm #{repository[:scm]}"
113
+ end
114
+
115
+ raise 'Error: cannot export secrets files: aborting' unless ok
116
+
117
+ secret[:digest] = Digest::SHA256.file(dest_fname).hexdigest
118
+ end
119
+
120
+ # Retrieve digests of secrets from application server
121
+ escaped_fnames = secrets_map.keys.collect { |fname| Shellwords.escape(fname) }
122
+ capture("cd #{shared_path.shellescape}; " \
123
+ "sha256sum #{escaped_fnames.join(' ')} || true").split("\n").each do |digest_line|
124
+ match = digest_line.match(/([0-9a-f]{64}) [ *](.*)/)
125
+ raise "Invalid digest returned: #{digest_line}" unless match && secrets_map.key?(match[2])
126
+
127
+ secrets_map[match[2]][:server_digest] = match[1]
128
+ end
129
+
130
+ # Upload replacements for all changed files
131
+ secrets_map.each_value do |secret|
132
+ if secret[:digest] == secret[:server_digest]
133
+ # Capistrano::CLI.ui.say "Unchanged secret: #{secret[:shared_dest]}"
134
+ next
135
+ end
136
+
137
+ Capistrano::CLI.ui.say "Uploading changed secret file: #{secret[:shared_dest]}"
138
+ changed << secret[:shared_dest]
139
+ # Capistrano does an in-place overwrite of the file, so use a temporary name,
140
+ # then move it into place
141
+ temp_dest = capture("mktemp -p #{shared_path.shellescape}").chomp
142
+ dest_fname = File.join(secret_dir, secret[:shared_dest])
143
+ put File.read(dest_fname), temp_dest
144
+ escape_shared_dest = Shellwords.escape(secret[:shared_dest])
145
+ escape_temp_dest = Shellwords.escape(temp_dest)
146
+ capture("cd #{shared_path.shellescape}; " \
147
+ "chmod 664 #{escape_temp_dest}; " \
148
+ "if [ -e #{escape_shared_dest} ]; then cp -p #{escape_shared_dest}{,.orig}; fi; " \
149
+ "mv #{escape_temp_dest} #{escape_shared_dest}")
150
+ end
151
+ end
152
+
153
+ if changed.empty?
154
+ Capistrano::CLI.ui.say 'No changed secret files to upload'
155
+ else
156
+ Capistrano::CLI.ui.say "Uploaded #{changed.size} changed secret files: #{changed.join(', ')}"
157
+ end
158
+ # TODO: Support logging of changes, so that a calling script can report changes
159
+
160
+ # TODO: maintain a per-target local cache of latest revisions uploaded / file checksums
161
+ # then we don't need to re-connect to the remote servers, if nothing changed,
162
+ # We could also then only need to do "svn ls" instead of "svn export"
163
+
164
+ # TODO: Warn if some repos are inaccessible?
165
+ # TODO: Add notes for passwordless SSH deployment, using ssh-agent
166
+ end
167
+ end
168
+ end
@@ -2,7 +2,9 @@ require 'rainbow'
2
2
 
3
3
  # Discrete bits of functionality we use automatically:
4
4
  require_relative 'assets'
5
+ require_relative 'deploy_secrets'
5
6
  require_relative 'install_ruby'
7
+ require_relative 'preinstall'
6
8
  require_relative 'restart'
7
9
  require_relative 'revision_logger'
8
10
  require_relative 'ruby_version'
@@ -52,11 +54,15 @@ Capistrano::Configuration.instance(:must_exist).load do
52
54
  # sticky; all deployments made within it should be owned by the deployer group too. This
53
55
  # means that e.g. a deployment by "bob.smith" can then be rolled back by "tom.jones".
54
56
  run "mkdir -p #{deploy_to}"
55
- run "chgrp -R deployer #{deploy_to}"
57
+ # Set deployer group for everything created by this user
58
+ # run "chgrp -R deployer #{deploy_to}"
59
+ run "find #{deploy_to} -group #{fetch(:user)} -print0 |xargs -r0 chgrp -h deployer"
56
60
 
57
61
  # The sticky group will apply automatically to new subdirectories, but
58
62
  # any existing subdirectories will need it manually applying via `-R`.
59
- run "chmod -R g+s #{deploy_to}"
63
+ # run "chmod -R g+s #{deploy_to}"
64
+ run "find #{deploy_to} -user #{fetch(:user)} -type d " \
65
+ '-not -perm -2000 -print0 |xargs -r0 chmod g+s'
60
66
  end
61
67
 
62
68
  desc 'Custom tasks to be run once, after the initial `cap setup`'
@@ -66,8 +72,12 @@ Capistrano::Configuration.instance(:must_exist).load do
66
72
  run "mkdir -p #{full_path}"
67
73
 
68
74
  # Allow the application to write into here:
69
- run "chgrp -R #{application_group} #{full_path}"
70
- run "chmod -R g+s #{full_path}"
75
+ # run "chgrp -R #{application_group} #{full_path}"
76
+ # run "chmod -R g+s #{full_path}"
77
+ run "find #{full_path} -user #{fetch(:user)} -not -group #{application_group} " \
78
+ "-print0 |xargs -r0 chgrp -h #{application_group}"
79
+ run "find #{full_path} -user #{fetch(:user)} -type d " \
80
+ '-not -perm -2000 -print0 |xargs -r0 chmod g+s'
71
81
  end
72
82
 
73
83
  fetch(:shared_paths, []).each do |path|
@@ -184,6 +194,14 @@ def target_ruby_version_for(env)
184
194
  match ? match[:version] : raise('Unrecognized Ruby version!')
185
195
  end
186
196
 
197
+ def log_deployment_message(msg)
198
+ name = fetch(:deployer_name, capture('id -un').chomp)
199
+ log = File.join(shared_path, 'revisions.log')
200
+ msg = "[#{Time.now}] #{name} #{msg}" # rubocop:disable Rails/TimeZone
201
+
202
+ run "(test -e #{log} || (touch #{log} && chmod 664 #{log})) && echo #{Shellwords.escape(msg)} >> #{log};"
203
+ end
204
+
187
205
  def add_target(env, name, app, port, app_user, is_web_server)
188
206
  desc "Deploy to #{env} service #{app_user || 'you'}@#{app}:#{port}"
189
207
  task(name) do
@@ -0,0 +1,37 @@
1
+ Capistrano::Configuration.instance(:must_exist).load do
2
+ namespace :deploy do
3
+ desc <<~DESC
4
+ Preinstall ruby and gems, then abort and rollback cleanly, leaving the
5
+ current installation unchanged.
6
+
7
+ This is particularly useful for ruby version bumps: installing the new
8
+ ruby version and all the bundled gems can take a long time.
9
+
10
+ This aborts before updating out-of-bundle gems, in case that causes
11
+ issues when restarting the currently installed version.
12
+
13
+ Usage:
14
+ cap target deploy:preinstall
15
+ DESC
16
+ task :preinstall do
17
+ # Running this task sets a flag, to make ndr_dev_support:check_preinstall abort.
18
+ # We do this in a roundabout way on Capistrano 2, because deploy:update_code
19
+ # explicitly runs deploy:finalize_update, instead of using task dependencies.
20
+ set :preinstall, true
21
+ end
22
+ end
23
+
24
+ namespace :ndr_dev_support do
25
+ desc 'Hook to abort capistrano installation early after preinstalling ruby and in-bundle gems'
26
+ task :check_preinstall do
27
+ next unless fetch(:preinstall, false)
28
+
29
+ log_deployment_message("preinstalled #{real_revision}")
30
+ warn Rainbow("Successful preinstall for target: #{fetch(:name)}")
31
+ abort 'Aborting after successful preinstall'
32
+ end
33
+ end
34
+
35
+ after 'deploy:preinstall', 'deploy:update'
36
+ before 'ndr_dev_support:update_out_of_bundle_gems', 'ndr_dev_support:check_preinstall'
37
+ end
@@ -2,11 +2,7 @@ Capistrano::Configuration.instance(:must_exist).load do
2
2
  namespace :ndr_dev_support do
3
3
  desc 'Append to the log of deployments the user and revision.'
4
4
  task :log_deployment, except: { no_release: true } do
5
- name = fetch(:deployer_name, capture('id -un'))
6
- log = File.join(shared_path, 'revisions.log')
7
- msg = "[#{Time.now}] #{name} deployed #{latest_revision}"
8
-
9
- run "(test -e #{log} || (touch #{log} && chmod 664 #{log})) && echo #{msg} >> #{log};"
5
+ log_deployment_message("deployed #{latest_revision}")
10
6
  end
11
7
  end
12
8
 
@@ -2,5 +2,5 @@
2
2
  # This defines the NdrDevSupport version. If you change it, rebuild and commit the gem.
3
3
  # Use "rake build" to build the gem, see rake -T for all bundler rake tasks (and our own).
4
4
  module NdrDevSupport
5
- VERSION = '7.3.0'
5
+ VERSION = '7.3.2'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ndr_dev_support
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.3.0
4
+ version: 7.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - NCRS Development Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-19 00:00:00.000000000 Z
11
+ date: 2025-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -409,8 +409,10 @@ files:
409
409
  - lib/minitest/rake_ci_plugin.rb
410
410
  - lib/ndr_dev_support.rb
411
411
  - lib/ndr_dev_support/capistrano/assets.rb
412
+ - lib/ndr_dev_support/capistrano/deploy_secrets.rb
412
413
  - lib/ndr_dev_support/capistrano/install_ruby.rb
413
414
  - lib/ndr_dev_support/capistrano/ndr_model.rb
415
+ - lib/ndr_dev_support/capistrano/preinstall.rb
414
416
  - lib/ndr_dev_support/capistrano/restart.rb
415
417
  - lib/ndr_dev_support/capistrano/revision_logger.rb
416
418
  - lib/ndr_dev_support/capistrano/ruby_version.rb
@@ -485,7 +487,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
485
487
  - !ruby/object:Gem::Version
486
488
  version: '0'
487
489
  requirements: []
488
- rubygems_version: 3.3.27
490
+ rubygems_version: 3.4.19
489
491
  signing_key:
490
492
  specification_version: 4
491
493
  summary: NDR Developer Support library