vagrant-docker-certificates-manager 0.3.0 → 0.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: 1f64aa238bae8dbe7da5f4b4554bc04baa2d70c955384ff64811e03ef684c791
4
- data.tar.gz: ac5625a4e633a4e487ff814307920171db797da2a87a7f384da04f616d91c7e8
3
+ metadata.gz: 7205874687ebbce5453b9267447c8a007d949416b370e67d141b4e633193ce1b
4
+ data.tar.gz: 5970d32848bbb46f63f4d3b1dd0c12823350937e69363dbd1c4e1d7e7cf1ab16
5
5
  SHA512:
6
- metadata.gz: 4cb997ec0c8d99ed7f221a634b0a0ff0e88aab81516d496c4b541adde7a1e85a88a9ea4ed6c902fafa593a8be6357f5706113def410561cb2b8eb22461a25745
7
- data.tar.gz: 638c3572ab9ffa1d6ce7a7c7c4605022c41f257ffc801b30ed7fc3ee7a4d30a118b7a8b1871cf27f93c7b24308f5aca6d0db49810431ba8074bc54de58b275be
6
+ metadata.gz: da2f09201144ccaa74456e248c84d3c89297be73764c7d7cd2cc5a5fda5547c08b03d1eb9099fdab2808bf7a51e392faab5496ed3033ef86a4e96c9121388e88
7
+ data.tar.gz: 8a8f01b14910172242e72b62bd9dd889fcc79f56e0449f2d48ccffd763c91f5dab7a887ca52b214efdd8038d976c7e0c9464d4069b95c03ecd226fa1f30ebefd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0](https://github.com/julienpoirou/vagrant-docker-certificates-manager/compare/v0.3.0...v0.4.0) (2026-07-03)
4
+
5
+
6
+ ### Fonctionnalités ✨
7
+
8
+ * Improve plugin quality and publish RubyDoc ([278298b](https://github.com/julienpoirou/vagrant-docker-certificates-manager/commit/278298b614ac9e4b0797d806bed3e0015d7fdb1f))
9
+
10
+
11
+ ### Corrections 🐛
12
+
13
+ * Resolve rubocop offenses and enable pages for RubyDoc deploy ([227c596](https://github.com/julienpoirou/vagrant-docker-certificates-manager/commit/227c5967c1480a2771413cf5738d2ac1f030d495))
14
+
3
15
  ## [0.3.0](https://github.com/julienpoirou/vagrant-docker-certificates-manager/compare/v0.2.0...v0.3.0) (2026-03-09)
4
16
 
5
17
 
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.4.0
@@ -24,6 +24,15 @@ module VagrantDockerCertificatesManager
24
24
  @app.call(env)
25
25
  end
26
26
 
27
+ # Installs a configured certificate into the current host trust store.
28
+ #
29
+ # The operation is idempotent: when the fingerprint is already tracked,
30
+ # the current machine adopts the existing registry entry instead of
31
+ # reinstalling the certificate.
32
+ #
33
+ # @param cfg [#cert_path, #cert_name] Vagrant certificate configuration.
34
+ # @param env [Hash, nil] Vagrant environment hash.
35
+ # @return [Hash] Normalized result with code, status, data, or error.
27
36
  def self.perform_install(cfg, env)
28
37
  unless File.file?(cfg.cert_path)
29
38
  return { code: 1, status: "error",
@@ -32,9 +41,11 @@ error: UiHelpers.t("errors.invalid_path", path: cfg.cert_path) }
32
41
 
33
42
  name = cfg.cert_name.to_s.strip.empty? ? Cert.default_name_from(cfg.cert_path) : cfg.cert_name
34
43
  fp = Cert.sha1(cfg.cert_path)
44
+ mid = env && env[:machine]&.id
45
+
35
46
  if Registry.all.key?(fp)
36
- return { code: 1, status: "error",
37
- error: UiHelpers.t("errors.already_present", name: name) }
47
+ Registry.adopt(fp, mid) if mid
48
+ return { code: 0, status: "success", data: { os: OS.detect, cert: name, already: true } }
38
49
  end
39
50
 
40
51
  os = OS.detect
@@ -51,7 +62,8 @@ firefox: cfg.manage_firefox)
51
62
  "path" => File.expand_path(cfg.cert_path),
52
63
  "name" => name,
53
64
  "nickname" => Cert.nickname_for(name),
54
- "os" => os.to_s
65
+ "os" => os.to_s,
66
+ "owners" => [mid].compact
55
67
  })
56
68
  { code: 0, status: "success", data: { os: os, cert: name } }
57
69
  end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
3
4
  require_relative "../util/os"
4
5
  require_relative "../util/ui"
5
6
  require_relative "../util/cert"
7
+ require_relative "../util/generator"
6
8
  require_relative "../util/registry"
7
9
  require_relative "../helpers"
8
10
 
@@ -21,22 +23,61 @@ module VagrantDockerCertificatesManager
21
23
  result[:status] == "success" ? "uninstall.success" : "uninstall.fail",
22
24
  name: cfg.cert_name)
23
25
  end
26
+ purge_generated(cfg, env)
24
27
  @app.call(env)
25
28
  end
26
29
 
27
- def self.perform_uninstall(cfg, _env)
30
+ def purge_generated(cfg, env)
31
+ # Generated material is purged only through explicit env flags to avoid
32
+ # deleting a CA/server certificate that may be shared outside this action.
33
+ files = []
34
+ files += [Generator::CA_CERT, Generator::CA_KEY, Generator::CRL] if env_flag?("VDCM_PURGE_CA_ON_DESTROY")
35
+ files += [Generator::SRV_CRT, Generator::SRV_KEY] if env_flag?("VDCM_PURGE_SERVER_ON_DESTROY")
36
+ return if files.empty?
37
+
38
+ dir = cfg.cert_dir
39
+ files.uniq.each do |name|
40
+ path = File.join(dir, name)
41
+ next unless File.exist?(path)
42
+
43
+ File.delete(path)
44
+ Ui.say(env, :info, "purge.removed", path: path)
45
+ rescue StandardError => e
46
+ Ui.say(env, :warn, "purge.failed", path: path, error: e.message)
47
+ end
48
+ end
49
+
50
+ def env_flag?(name)
51
+ ENV[name].to_s == "1"
52
+ end
53
+
54
+ # Removes a configured certificate when the current machine is the last owner.
55
+ #
56
+ # Shared certificates stay installed until every machine recorded in the
57
+ # registry has released ownership.
58
+ #
59
+ # @param cfg [#cert_path, #cert_name] Vagrant certificate configuration.
60
+ # @param env [Hash, nil] Vagrant environment hash.
61
+ # @return [Hash] Normalized result with code, status, data, or error.
62
+ def self.perform_uninstall(cfg, env)
28
63
  fp_entry = Registry.find_by_path(cfg.cert_path)
29
64
  unless fp_entry
30
65
  return({ code: 1, status: "error",
31
66
  error: UiHelpers.t("errors.not_found_for_remove", path: cfg.cert_path) })
32
67
  end
33
68
  fp, rec = fp_entry
69
+ mid = env && env[:machine]&.id
70
+
71
+ if mid && Registry.release(fp, mid)
72
+ return({ code: 0, status: "success", data: { kept: true, cert: rec["name"] } })
73
+ end
74
+
34
75
  os = OS.detect
35
76
  ok = case os
36
77
  when :mac then OS.mac_remove_by_fp(fp)
37
78
  when :linux then OS.linux_uninstall_cert(rec["name"], nss: cfg.manage_nss_browsers,
38
79
  firefox: cfg.manage_firefox)
39
- when :windows then OS.win_remove_by_fp(fp)
80
+ when :windows then OS.win_remove_by_fp(fp, disable_firefox: !Registry.others_for_os?(fp, "windows"))
40
81
  else return({ code: 2, status: "error", error: UiHelpers.t("errors.os_unsupported") })
41
82
  end
42
83
  Registry.untrack(fp) if ok
@@ -1,37 +1,86 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VagrantDockerCertificatesManager
4
+ # Vagrant configuration for certificate generation, installation, and cleanup.
5
+ #
6
+ # @!attribute cert_path
7
+ # @return [String] Path to the CA certificate installed into the trust store.
8
+ # @!attribute cert_name
9
+ # @return [String] Friendly certificate name used in host trust stores.
10
+ # @!attribute install_on_up
11
+ # @return [Boolean] Whether to install the certificate during `vagrant up`.
12
+ # @!attribute remove_on_destroy
13
+ # @return [Boolean] Whether to remove the certificate during `vagrant destroy`.
14
+ # @!attribute manage_firefox
15
+ # @return [Boolean] Whether Firefox stores should be managed where supported.
16
+ # @!attribute manage_nss_browsers
17
+ # @return [Boolean] Whether NSS browser stores should be managed where supported.
18
+ # @!attribute generate_on_up
19
+ # @return [Boolean] Whether local certificate material should be generated during `vagrant up`.
4
20
  class Config < Vagrant.plugin("2", :config)
5
21
  attr_accessor :cert_path, :cert_name, :install_on_up, :remove_on_destroy,
6
22
  :manage_firefox, :manage_nss_browsers, :locale, :verbose,
7
- :container_name
23
+ :container_name,
24
+ :generate_on_up, :ca_cn, :ca_days, :server_domain, :crl_url
8
25
 
9
26
  def initialize
10
- @cert_path = "certs/rootca.cert.pem"
11
- @cert_name = "local.dev"
12
- @install_on_up = false
13
- @remove_on_destroy = false
14
- @manage_firefox = false
15
- @manage_nss_browsers = true
16
- @locale = "en"
17
- @verbose = false
18
- @container_name = nil
27
+ @cert_path = UNSET_VALUE
28
+ @cert_name = UNSET_VALUE
29
+ @install_on_up = UNSET_VALUE
30
+ @remove_on_destroy = UNSET_VALUE
31
+ @manage_firefox = UNSET_VALUE
32
+ @manage_nss_browsers = UNSET_VALUE
33
+ @locale = UNSET_VALUE
34
+ @verbose = UNSET_VALUE
35
+ @container_name = UNSET_VALUE
36
+ @generate_on_up = UNSET_VALUE
37
+ @ca_cn = UNSET_VALUE
38
+ @ca_days = UNSET_VALUE
39
+ @server_domain = UNSET_VALUE
40
+ @crl_url = UNSET_VALUE
19
41
  end
20
42
 
21
43
  def finalize!
44
+ @cert_path = "certs/rootca.cert.pem" if @cert_path == UNSET_VALUE
45
+ @cert_name = "local.dev" if @cert_name == UNSET_VALUE
46
+ @install_on_up = false if @install_on_up == UNSET_VALUE
47
+ @remove_on_destroy = false if @remove_on_destroy == UNSET_VALUE
48
+ @manage_firefox = false if @manage_firefox == UNSET_VALUE
49
+ @manage_nss_browsers = true if @manage_nss_browsers == UNSET_VALUE
50
+ @locale = "en" if @locale == UNSET_VALUE
51
+ @verbose = false if @verbose == UNSET_VALUE
52
+ @container_name = nil if @container_name == UNSET_VALUE
53
+ @generate_on_up = false if @generate_on_up == UNSET_VALUE
54
+ @ca_cn = "local-ca" if @ca_cn == UNSET_VALUE
55
+ @ca_days = 3650 if @ca_days == UNSET_VALUE
56
+ @server_domain = nil if @server_domain == UNSET_VALUE
57
+ @crl_url = nil if @crl_url == UNSET_VALUE
58
+
22
59
  @install_on_up = !!@install_on_up
23
60
  @remove_on_destroy = !!@remove_on_destroy
24
61
  @manage_firefox = !!@manage_firefox
25
62
  @manage_nss_browsers = !!@manage_nss_browsers
26
63
  @verbose = !!@verbose
64
+ @generate_on_up = !!@generate_on_up
27
65
  @locale = (@locale || "en").to_s
66
+ @ca_cn = (@ca_cn || "local-ca").to_s
67
+ @ca_days = @ca_days.to_i
28
68
  end
29
69
 
30
70
  def validate(_machine)
31
71
  errors = []
32
72
  errors << "cert_path must be provided" if @cert_path.to_s.strip.empty?
33
73
  errors << "cert_name must be provided" if @cert_name.to_s.strip.empty?
74
+ unless @locale.is_a?(String) && %w[en fr].include?(@locale.to_s[0, 2].downcase)
75
+ errors << "locale must be 'en' or 'fr'"
76
+ end
77
+ errors << "ca_days must be a positive integer" if @generate_on_up && @ca_days.to_i <= 0
34
78
  { "vagrant-docker-certificates-manager" => errors }
35
79
  end
80
+
81
+ def cert_dir
82
+ d = File.dirname(@cert_path.to_s)
83
+ d.empty? ? "certs" : d
84
+ end
36
85
  end
37
86
  end
@@ -31,7 +31,11 @@ module VagrantDockerCertificatesManager
31
31
  return if defined?(@i18n_setup) && @i18n_setup
32
32
  ::I18n.enforce_available_locales = false
33
33
  base = File.expand_path("../../locales", __dir__)
34
- ::I18n.load_path |= Dir[File.join(base, "*.yml")]
34
+ paths = Dir[File.join(base, "*.yml")]
35
+ if paths.empty? && File.directory?(base)
36
+ paths = Dir.children(base).grep(/\.ya?ml\z/).map { |file| File.join(base, file) }
37
+ end
38
+ ::I18n.load_path |= paths
35
39
  ::I18n.available_locales = SUPPORTED
36
40
  default = ((ENV["VDCM_LANG"] || ENV["LANG"] || "en")[0, 2] rescue "en").to_sym
37
41
  ::I18n.default_locale = SUPPORTED.include?(default) ? default : :en
@@ -98,8 +102,6 @@ module VagrantDockerCertificatesManager
98
102
  ENV["VDCM_DEBUG"].to_s == "1"
99
103
  end
100
104
 
101
- # ── display ───────────────────────────────────────────────────────────────
102
-
103
105
  def level_to_emoji(level)
104
106
  case level
105
107
  when :success then :success
@@ -130,8 +132,6 @@ module VagrantDockerCertificatesManager
130
132
  say(env_or_ui, :info, nil, raw: "#{e(:bug)} #{msg}")
131
133
  end
132
134
 
133
- # ── help ──────────────────────────────────────────────────────────────────
134
-
135
135
  def print_general_help(no_emoji: false, ui: nil)
136
136
  setup_i18n!
137
137
  lines = []
@@ -139,6 +139,10 @@ module VagrantDockerCertificatesManager
139
139
  lines << " #{t('cli.usage')}"
140
140
  t_hash("help.commands").each_value { |line| lines << " #{line}" }
141
141
 
142
+ emit_lines(lines, ui)
143
+ end
144
+
145
+ def emit_lines(lines, ui)
142
146
  if ui
143
147
  lines.each { |ln| ui.info(ln) }
144
148
  else
@@ -151,39 +155,53 @@ module VagrantDockerCertificatesManager
151
155
  topic = (topic || "").to_s.strip.downcase
152
156
  return print_general_help(no_emoji: no_emoji, ui: ui) if topic.empty?
153
157
 
154
- base = "help.topic.#{topic}"
155
- title = t("#{base}.title", default: nil)
156
- usage = t("#{base}.usage", default: nil)
157
- desc = t("#{base}.description", default: nil)
158
- opts = t_hash("#{base}.options")
159
- exs = ::I18n.t(ns_key("#{base}.examples"), default: [])
158
+ fields = topic_help_fields("help.topic.#{topic}")
159
+ return print_general_help(no_emoji: no_emoji, ui: ui) if topic_help_empty?(fields)
160
160
 
161
- if title.nil? && usage.nil? && desc.nil? && opts.empty? && exs.empty?
162
- return print_general_help(no_emoji: no_emoji, ui: ui)
163
- end
161
+ emit_lines(build_topic_help_lines(topic, fields, no_emoji), ui)
162
+ end
164
163
 
164
+ def topic_help_fields(base)
165
+ {
166
+ title: t("#{base}.title", default: nil),
167
+ usage: t("#{base}.usage", default: nil),
168
+ desc: t("#{base}.description", default: nil),
169
+ opts: t_hash("#{base}.options"),
170
+ exs: ::I18n.t(ns_key("#{base}.examples"), default: [])
171
+ }
172
+ end
173
+
174
+ def topic_help_empty?(fields)
175
+ fields[:title].nil? && fields[:usage].nil? && fields[:desc].nil? &&
176
+ fields[:opts].empty? && fields[:exs].empty?
177
+ end
178
+
179
+ def build_topic_help_lines(topic, fields, no_emoji)
165
180
  lines = []
166
- lines << "#{e(:info, no_emoji: no_emoji)} #{title || t('help.topic_fallback_title', topic: topic)}"
181
+ lines << "#{e(:info, no_emoji: no_emoji)} #{fields[:title] || t('help.topic_fallback_title', topic: topic)}"
167
182
  lines << " #{t('help.usage_label')}"
168
- lines << " #{usage || 'vagrant certs help'}"
169
- if desc && !desc.strip.empty?
170
- lines << " #{t('help.description_label')}"
171
- lines << " #{desc}"
172
- end
173
- unless opts.empty?
174
- lines << " #{t('help.options_label')}"
175
- opts.each_value { |line| lines << " #{line}" }
183
+ lines << " #{fields[:usage] || 'vagrant certs help'}"
184
+
185
+ desc = fields[:desc]
186
+ append_help_section(lines, t("help.description_label"), [" #{desc}"]) if desc && !desc.strip.empty?
187
+ unless fields[:opts].empty?
188
+ append_help_section(lines, t("help.options_label"), fields[:opts].values.map do |l|
189
+ " #{l}"
190
+ end)
176
191
  end
192
+
193
+ exs = fields[:exs]
177
194
  if exs.is_a?(Array) && !exs.empty?
178
- lines << " #{t('help.examples_label')}"
179
- exs.each { |ex| lines << " #{ex}" }
195
+ append_help_section(lines, t("help.examples_label"), exs.map do |ex|
196
+ " #{ex}"
197
+ end)
180
198
  end
199
+ lines
200
+ end
181
201
 
182
- if ui
183
- lines.each { |ln| ui.info(ln) }
184
- else
185
- lines.each { |ln| puts ln }
186
- end
202
+ def append_help_section(lines, label, body_lines)
203
+ lines << " #{label}"
204
+ body_lines.each { |line| lines << line }
187
205
  end
188
206
  end
189
207
  end
@@ -31,5 +31,99 @@ module VagrantDockerCertificatesManager
31
31
  def nickname_for(name)
32
32
  "#{MARKER}:#{name}"
33
33
  end
34
+
35
+ def fingerprint_of(cert)
36
+ OpenSSL::Digest::SHA1.hexdigest(cert.to_der).upcase
37
+ end
38
+
39
+ # Generates a local development certificate authority.
40
+ #
41
+ # The generated CA is intended for local Docker/Vagrant workflows, not as a
42
+ # public browser-trusted certificate authority.
43
+ #
44
+ # @param cn [String] Common name for the CA certificate.
45
+ # @param days [Integer] Validity period in days.
46
+ # @param org [String] Organization attribute written to the subject.
47
+ # @param country [String] Country attribute written to the subject.
48
+ # @return [Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>] Generated certificate and private key.
49
+ def generate_ca(common_name:, days: 3650, org: "VDCM", country: "FR")
50
+ key = OpenSSL::PKey::RSA.generate(2048)
51
+ cert = OpenSSL::X509::Certificate.new
52
+
53
+ cert.version = 2
54
+ cert.serial = 1
55
+ cert.subject = OpenSSL::X509::Name.parse("/CN=#{common_name}/O=#{org}/C=#{country}")
56
+ cert.issuer = cert.subject
57
+ cert.public_key = key.public_key
58
+ cert.not_before = Time.now
59
+ cert.not_after = Time.now + (days * 24 * 60 * 60)
60
+
61
+ ef = OpenSSL::X509::ExtensionFactory.new(cert, cert)
62
+ cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true))
63
+ cert.add_extension(ef.create_extension("keyUsage", "keyCertSign,cRLSign", true))
64
+ cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash"))
65
+ cert.sign(key, OpenSSL::Digest.new("SHA256"))
66
+
67
+ [cert, key]
68
+ end
69
+
70
+ def load_ca(cert_path, key_path)
71
+ [
72
+ OpenSSL::X509::Certificate.new(File.read(cert_path)),
73
+ OpenSSL::PKey::RSA.new(File.read(key_path))
74
+ ]
75
+ end
76
+
77
+ # Generates a server certificate signed by the supplied local CA.
78
+ #
79
+ # Modern clients validate the SAN extension; the CN is kept mainly for
80
+ # readability and compatibility with older tooling.
81
+ #
82
+ # @param ca_cert [OpenSSL::X509::Certificate] CA certificate used as issuer.
83
+ # @param ca_key [OpenSSL::PKey::RSA] CA private key used to sign the certificate.
84
+ # @param domain [String] DNS name written to CN and subjectAltName.
85
+ # @param days [Integer] Validity period in days.
86
+ # @param crl_url [String, nil] Optional CRL distribution point URL.
87
+ # @return [Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>] Generated certificate and private key.
88
+ def generate_server(ca_cert, ca_key, domain:, days: 825, crl_url: nil)
89
+ key = OpenSSL::PKey::RSA.generate(2048)
90
+ cert = OpenSSL::X509::Certificate.new
91
+
92
+ cert.version = 2
93
+ cert.serial = 2
94
+ cert.subject = OpenSSL::X509::Name.parse("/CN=#{domain}")
95
+ cert.issuer = ca_cert.subject
96
+ cert.public_key = key.public_key
97
+ cert.not_before = Time.now
98
+ cert.not_after = Time.now + (days * 24 * 60 * 60)
99
+
100
+ ef = OpenSSL::X509::ExtensionFactory.new(ca_cert, cert)
101
+ cert.add_extension(ef.create_extension("subjectAltName", "DNS:#{domain}"))
102
+ cert.add_extension(ef.create_extension("basicConstraints", "CA:FALSE"))
103
+ cert.add_extension(ef.create_extension("keyUsage", "digitalSignature,keyEncipherment", true))
104
+ cert.add_extension(ef.create_extension("extendedKeyUsage", "serverAuth"))
105
+ unless crl_url.to_s.strip.empty?
106
+ cert.add_extension(ef.create_extension("crlDistributionPoints", "URI:#{crl_url}"))
107
+ end
108
+ cert.sign(ca_key, OpenSSL::Digest.new("SHA256"))
109
+
110
+ [cert, key]
111
+ end
112
+
113
+ # Generates an empty certificate revocation list for the local CA.
114
+ #
115
+ # @param ca_cert [OpenSSL::X509::Certificate] CA certificate used as issuer.
116
+ # @param ca_key [OpenSSL::PKey::RSA] CA private key used to sign the CRL.
117
+ # @param days [Integer] Validity period in days.
118
+ # @return [OpenSSL::X509::CRL] Signed CRL.
119
+ def generate_crl(ca_cert, ca_key, days: 3650)
120
+ crl = OpenSSL::X509::CRL.new
121
+ crl.issuer = ca_cert.subject
122
+ crl.version = 1
123
+ crl.last_update = Time.now
124
+ crl.next_update = Time.now + (days * 24 * 60 * 60)
125
+ crl.sign(ca_key, OpenSSL::Digest.new("SHA256"))
126
+ crl
127
+ end
34
128
  end
35
129
  end
@@ -15,6 +15,12 @@ module VagrantDockerCertificatesManager
15
15
  FileUtils.mkdir_p(File.dirname(db_path))
16
16
  end
17
17
 
18
+ # Loads tracked certificates from the best-effort local registry.
19
+ #
20
+ # A corrupt or missing registry must not block Vagrant actions, because the
21
+ # OS trust store remains the source of truth.
22
+ #
23
+ # @return [Hash] Registry data keyed by certificate fingerprint.
18
24
  def load
19
25
  ensure_dir!
20
26
  return {} unless File.exist?(db_path)
@@ -28,6 +34,11 @@ module VagrantDockerCertificatesManager
28
34
  File.write(db_path, JSON.pretty_generate(data))
29
35
  end
30
36
 
37
+ # Tracks a certificate fingerprint and its ownership metadata.
38
+ #
39
+ # @param fp [String] Certificate fingerprint.
40
+ # @param attrs [Hash] Persisted metadata such as path, name, OS, and owners.
41
+ # @return [void]
31
42
  def track(fp, attrs)
32
43
  data = load
33
44
  data[fp] = attrs
@@ -41,6 +52,42 @@ module VagrantDockerCertificatesManager
41
52
  removed
42
53
  end
43
54
 
55
+ # Adds an owner to an already tracked certificate.
56
+ #
57
+ # @param fp [String] Certificate fingerprint.
58
+ # @param owner [String, #to_s] Vagrant machine id adopting the certificate.
59
+ # @return [Boolean] Whether the certificate was already tracked.
60
+ # rubocop:disable Naming/PredicateMethod -- boolean return kept for symmetry with #release
61
+ def adopt(fp, owner)
62
+ data = load
63
+ return false unless data[fp]
64
+ data[fp]["owners"] = Array(data[fp]["owners"]) | [owner.to_s]
65
+ save(data)
66
+ true
67
+ end
68
+ # rubocop:enable Naming/PredicateMethod
69
+
70
+ # Releases one owner from a tracked certificate.
71
+ #
72
+ # Keep the certificate installed while at least one Vagrant machine still
73
+ # owns it.
74
+ #
75
+ # @param fp [String] Certificate fingerprint.
76
+ # @param owner [String, #to_s] Vagrant machine id releasing the certificate.
77
+ # @return [Boolean] Whether other owners remain after the release.
78
+ # rubocop:disable Naming/PredicateMethod -- boolean return kept for symmetry with #adopt
79
+ def release(fp, owner)
80
+ data = load
81
+ rec = data[fp]
82
+ return false unless rec
83
+ owners = Array(rec["owners"])
84
+ owners.delete(owner.to_s)
85
+ rec["owners"] = owners
86
+ save(data)
87
+ !owners.empty?
88
+ end
89
+ # rubocop:enable Naming/PredicateMethod
90
+
44
91
  def find_by_path(path)
45
92
  data = load
46
93
  data.find { |_fp, v| File.expand_path(v["path"]) == File.expand_path(path) }
@@ -49,5 +96,9 @@ module VagrantDockerCertificatesManager
49
96
  def all
50
97
  load
51
98
  end
99
+
100
+ def others_for_os?(fp, os)
101
+ load.any? { |k, v| k != fp && v["os"].to_s == os.to_s }
102
+ end
52
103
  end
53
104
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vagrant-docker-certificates-manager
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julien Poirou
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-09 00:00:00.000000000 Z
11
+ date: 2026-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: i18n