ndr_dev_support 7.3.0 → 7.3.1

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: 41d7ba2652d9fa563d40304d74636a120fabce678f2dac3247be956ab448e477
4
- data.tar.gz: 52be1d6408975a0777733c5ab9007098ffe2128360b00f13857c591ac051a0db
3
+ metadata.gz: 5084b3677de82f1613b2a3d95e2290ca79128285da4a004636ee67d23d1546d5
4
+ data.tar.gz: 13561b93f13f6739c072382faf65968f59a5b82898ccbbb9b3a3c55a006744c3
5
5
  SHA512:
6
- metadata.gz: d83ee219968cfbffb0f3b2eb9615442c595d6013ba1e0c21f04421197cc68dd94df9338e152922b1e39a566dd4ce7a35543c9672eb6b21548f3f80688b82610a
7
- data.tar.gz: fc6eb48554bd132c6bc9bf852ca5d708baaae03c61e36a0dfd8880665d3859418d3ba28a179939a801ba9262c956c41b5db06e5c8f11fd904da7870499c5138f
6
+ metadata.gz: 656ead5b1726958c845ba2eda14afc1e548e1a012a813aff524dc78953630bae0f119bacdaed63b3621c4da6bfd41439f3022c8ab260a43470408b0f1ef51080
7
+ data.tar.gz: 541c54385afa983aaa6087302f92318d425556aaf5d9de534c685e52e7dc770e799bc318f0983d83c73053c1d81fe4b7937f14d91143b156292b4387481b5201
data/CHANGELOG.md CHANGED
@@ -1,6 +1,10 @@
1
1
  ## [Unreleased]
2
2
  *no unreleased changes*
3
3
 
4
+ ## 7.3.1 / 2025-01-02
5
+ ### Added
6
+ * Capistrano: deploy application secrets from a subversion or git repository
7
+
4
8
  ## 7.3.0 / 2024-12-19
5
9
  ### Added
6
10
  * Capistrano: install rbenv and ruby from /opt/rbenv.tar.gz or vendor/rbenv/
@@ -0,0 +1,166 @@
1
+ # Add a git or svn secrets respository for ndr_dev_support:deploy_secrets
2
+ def add_secrets_repo(name:, url:, scm:, branch: nil)
3
+ raise "Invalid repo name #{name}" unless /\A[A-Z0-9_-]+\z/i.match?(name)
4
+ raise "Unknown scm #{scm}" unless %w[svn git].include?(scm)
5
+ raise "Expected branch for repo #{name}" if scm == 'git' && branch.to_s.empty?
6
+
7
+ secrets_repositories = fetch(:secrets_repositories, {})
8
+ secrets_repositories[name] = { url: url, scm: scm, branch: branch }
9
+ set :secrets_repositories, secrets_repositories
10
+ end
11
+
12
+ # Add a secret to be deployed by ndr_dev_support:deploy_secrets
13
+ def add_secret(repo:, repo_path:, shared_dest:)
14
+ secrets = fetch(:secrets, [])
15
+ raise "Unknown repo #{repo}" unless fetch(:secrets_repositories, {}).key?(repo)
16
+
17
+ secrets << { repo: repo, repo_path: repo_path, shared_dest: shared_dest }
18
+ set :secrets, secrets
19
+ end
20
+
21
+ Capistrano::Configuration.instance(:must_exist).load do
22
+ namespace :ndr_dev_support do
23
+ desc <<~DESC
24
+ Deploy updated application secrets to shared folders on application servers
25
+
26
+ To use this in a project, add something like the code below to your
27
+ Capistrano file config/deploy.rb, then run:
28
+ $ cap target app:update_secrets
29
+
30
+ namespace :app do
31
+ desc 'Update application secrets'
32
+ task :update_secrets do
33
+ add_secrets_repo(name: 'userlists',
34
+ url: 'https://github.com/example/users.git',
35
+ branch: 'main',
36
+ scm: 'git')
37
+ add_secrets_repo(name: 'encrypted_credentials_store',
38
+ url: 'https://svn-server.example.org/svn/creds', scm: 'svn')
39
+
40
+ add_secret(repo: 'encrypted_credentials_store',
41
+ repo_path: 'path/to/credentials.yml.enc',
42
+ shared_dest: 'config/credentials.yml.enc')
43
+ add_secret(repo: 'userlists',
44
+ repo_path: 'config/userlist.yml',
45
+ shared_dest: 'config/userlist.yml')
46
+ end
47
+ end
48
+ after 'app:update_secrets', 'ndr_dev_support:deploy_secrets'
49
+ DESC
50
+ task :deploy_secrets do
51
+ # List of repositories used for secrets
52
+ secrets_repositories = fetch(:secrets_repositories, {})
53
+ secrets = fetch(:secrets, [])
54
+ secrets_repo_base = Pathname.new('tmp/deployment-secrets')
55
+
56
+ if secrets.empty?
57
+ Capistrano::CLI.ui.say 'Warning: No secret files configured to upload'
58
+ next
59
+ end
60
+
61
+ # Allow quick indexing by filename
62
+ secrets_map = secrets.to_h { |secret| [secret[:shared_dest], secret] } # rubocop:disable Rails/IndexBy
63
+ changed = [] # List of changed files updated
64
+ Dir.mktmpdir do |secret_dir|
65
+ # Clone git secrets repositories if required
66
+ used_repos = secrets.collect { |secret| secret[:repo] }.uniq
67
+ repo_dirs = {}
68
+ used_repos.each do |repo|
69
+ repository = secrets_repositories[repo]
70
+ next unless repository[:scm] == 'git'
71
+
72
+ repo_dir = Pathname.new(secrets_repo_base).join(".git-#{repo}").to_s
73
+ if File.directory?(repo_dir)
74
+ ok = system("cd #{Shellwords.escape(repo_dir)} && git fetch")
75
+ raise "Error: cannot fetch secrets repository #{repo}: aborting" unless ok
76
+ else
77
+ ok = system('git', 'clone', '--mirror', '--filter=blob:none', repository[:url], repo_dir)
78
+ raise "Error: cannot clone secrets repository #{repo}: aborting" unless ok
79
+ end
80
+ repo_dirs[repo] = repo_dir
81
+ end
82
+
83
+ # Set up a temporary secrets directory of exported secrets,
84
+ # creating nested structure if necessary
85
+ secrets_map.each_value do |secret|
86
+ repo = secret[:repo]
87
+ repository = secrets_repositories[repo]
88
+ raise "Unknown repository #{secret[:repo]}" unless repository
89
+
90
+ repo_root = repository[:url]
91
+ raise 'Unknown / unsupported repository' unless repo_root&.start_with?('https://')
92
+
93
+ dest_fname = File.join(secret_dir, secret[:shared_dest])
94
+ dest_dir = File.dirname(dest_fname)
95
+ FileUtils.mkdir_p(dest_dir)
96
+ case repository[:scm]
97
+ when 'git'
98
+ repo_dir = Pathname.new(secrets_repo_base).join(".git-#{repo}").to_s
99
+ ok = system("GIT_DIR=#{Shellwords.escape(repo_dir)} git archive --format=tar " \
100
+ "#{Shellwords.escape(repository[:branch])} " \
101
+ "#{Shellwords.escape(secret[:repo_path])} | " \
102
+ "tar x -Ps %#{Shellwords.escape(secret[:repo_path])}%" \
103
+ "#{Shellwords.escape(File.join(secret_dir, secret[:shared_dest]))}% " \
104
+ "#{Shellwords.escape(secret[:repo_path])}")
105
+ when 'svn'
106
+ ok = system('svn', 'export', '--quiet', "#{repo_root}/#{secret[:repo_path]}",
107
+ File.join(secret_dir, secret[:shared_dest]))
108
+ # TODO: use --non-interactive, and then run again interactively if there's an eror
109
+ else
110
+ raise "Error: unsupported scm #{repository[:scm]}"
111
+ end
112
+
113
+ raise 'Error: cannot export secrets files: aborting' unless ok
114
+
115
+ secret[:digest] = Digest::SHA256.file(dest_fname).hexdigest
116
+ end
117
+
118
+ # Retrieve digests of secrets from application server
119
+ escaped_fnames = secrets_map.keys.collect { |fname| Shellwords.escape(fname) }
120
+ capture("cd #{shared_path.shellescape}; " \
121
+ "sha256sum #{escaped_fnames.join(' ')} || true").split("\n").each do |digest_line|
122
+ match = digest_line.match(/([0-9a-f]{64}) [ *](.*)/)
123
+ raise "Invalid digest returned: #{digest_line}" unless match && secrets_map.key?(match[2])
124
+
125
+ secrets_map[match[2]][:server_digest] = match[1]
126
+ end
127
+
128
+ # Upload replacements for all changed files
129
+ secrets_map.each_value do |secret|
130
+ if secret[:digest] == secret[:server_digest]
131
+ # Capistrano::CLI.ui.say "Unchanged secret: #{secret[:shared_dest]}"
132
+ next
133
+ end
134
+
135
+ Capistrano::CLI.ui.say "Uploading changed secret file: #{secret[:shared_dest]}"
136
+ changed << secret[:shared_dest]
137
+ # Capistrano does an in-place overwrite of the file, so use a temporary name,
138
+ # then move it into place
139
+ temp_dest = capture("mktemp -p #{shared_path.shellescape}").chomp
140
+ dest_fname = File.join(secret_dir, secret[:shared_dest])
141
+ put File.read(dest_fname), temp_dest
142
+ escape_shared_dest = Shellwords.escape(secret[:shared_dest])
143
+ escape_temp_dest = Shellwords.escape(temp_dest)
144
+ capture("cd #{shared_path.shellescape}; " \
145
+ "chmod 664 #{escape_temp_dest}; " \
146
+ "if [ -e #{escape_shared_dest} ]; then cp -p #{escape_shared_dest}{,.orig}; fi; " \
147
+ "mv #{escape_temp_dest} #{escape_shared_dest}")
148
+ end
149
+ end
150
+
151
+ if changed.empty?
152
+ Capistrano::CLI.ui.say 'No changed secret files to upload'
153
+ else
154
+ Capistrano::CLI.ui.say "Uploaded #{changed.size} changed secret files: #{changed.join(', ')}"
155
+ end
156
+ # TODO: Support logging of changes, so that a calling script can report changes
157
+
158
+ # TODO: maintain a per-target local cache of latest revisions uploaded / file checksums
159
+ # then we don't need to re-connect to the remote servers, if nothing changed,
160
+ # We could also then only need to do "svn ls" instead of "svn export"
161
+
162
+ # TODO: Warn if some repos are inaccessible?
163
+ # TODO: Add notes for passwordless SSH deployment, using ssh-agent
164
+ end
165
+ end
166
+ end
@@ -2,6 +2,7 @@ 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'
6
7
  require_relative 'restart'
7
8
  require_relative 'revision_logger'
@@ -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.1'
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.1
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-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -409,6 +409,7 @@ 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
414
415
  - lib/ndr_dev_support/capistrano/restart.rb