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 +4 -4
- data/CHANGELOG.md +15 -0
- data/config/rubocop/ndr.yml +1 -1
- data/lib/ndr_dev_support/capistrano/deploy_secrets.rb +168 -0
- data/lib/ndr_dev_support/capistrano/ndr_model.rb +22 -4
- data/lib/ndr_dev_support/capistrano/preinstall.rb +37 -0
- data/lib/ndr_dev_support/capistrano/revision_logger.rb +1 -5
- data/lib/ndr_dev_support/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb40a54108485d9f25a6cc88323fb474f6fd1b7ba7af787939dc8270bf7ceaf2
|
4
|
+
data.tar.gz: 8120053e06f2443829e917fda979987e9b09e8c568e75781876b338dbfa53ebb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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/
|
data/config/rubocop/ndr.yml
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
|
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.
|
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:
|
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.
|
490
|
+
rubygems_version: 3.4.19
|
489
491
|
signing_key:
|
490
492
|
specification_version: 4
|
491
493
|
summary: NDR Developer Support library
|