openvoxserver-ca 3.0.0.pre.rc1

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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +17 -0
  3. data/.github/release.yml +41 -0
  4. data/.github/workflows/gem_release.yaml +106 -0
  5. data/.github/workflows/prepare_release.yml +28 -0
  6. data/.github/workflows/release.yml +28 -0
  7. data/.github/workflows/unit_tests.yaml +45 -0
  8. data/.gitignore +14 -0
  9. data/.rspec +2 -0
  10. data/.travis.yml +16 -0
  11. data/CHANGELOG.md +15 -0
  12. data/CODEOWNERS +4 -0
  13. data/CODE_OF_CONDUCT.md +74 -0
  14. data/CONTRIBUTING.md +15 -0
  15. data/Gemfile +20 -0
  16. data/LICENSE +202 -0
  17. data/README.md +118 -0
  18. data/Rakefile +30 -0
  19. data/bin/console +14 -0
  20. data/bin/setup +8 -0
  21. data/exe/puppetserver-ca +10 -0
  22. data/lib/puppetserver/ca/action/clean.rb +109 -0
  23. data/lib/puppetserver/ca/action/delete.rb +286 -0
  24. data/lib/puppetserver/ca/action/enable.rb +140 -0
  25. data/lib/puppetserver/ca/action/generate.rb +330 -0
  26. data/lib/puppetserver/ca/action/import.rb +196 -0
  27. data/lib/puppetserver/ca/action/list.rb +253 -0
  28. data/lib/puppetserver/ca/action/migrate.rb +97 -0
  29. data/lib/puppetserver/ca/action/prune.rb +289 -0
  30. data/lib/puppetserver/ca/action/revoke.rb +108 -0
  31. data/lib/puppetserver/ca/action/setup.rb +188 -0
  32. data/lib/puppetserver/ca/action/sign.rb +146 -0
  33. data/lib/puppetserver/ca/certificate_authority.rb +418 -0
  34. data/lib/puppetserver/ca/cli.rb +145 -0
  35. data/lib/puppetserver/ca/config/puppet.rb +309 -0
  36. data/lib/puppetserver/ca/config/puppetserver.rb +84 -0
  37. data/lib/puppetserver/ca/errors.rb +40 -0
  38. data/lib/puppetserver/ca/host.rb +176 -0
  39. data/lib/puppetserver/ca/local_certificate_authority.rb +304 -0
  40. data/lib/puppetserver/ca/logger.rb +49 -0
  41. data/lib/puppetserver/ca/stub.rb +17 -0
  42. data/lib/puppetserver/ca/utils/cli_parsing.rb +67 -0
  43. data/lib/puppetserver/ca/utils/config.rb +61 -0
  44. data/lib/puppetserver/ca/utils/file_system.rb +109 -0
  45. data/lib/puppetserver/ca/utils/http_client.rb +232 -0
  46. data/lib/puppetserver/ca/utils/inventory.rb +84 -0
  47. data/lib/puppetserver/ca/utils/signing_digest.rb +27 -0
  48. data/lib/puppetserver/ca/version.rb +5 -0
  49. data/lib/puppetserver/ca/x509_loader.rb +170 -0
  50. data/lib/puppetserver/ca.rb +7 -0
  51. data/openvoxserver-ca.gemspec +31 -0
  52. data/tasks/spec.rake +15 -0
  53. data/tasks/vox.rake +19 -0
  54. metadata +154 -0
@@ -0,0 +1,232 @@
1
+ require 'net/https'
2
+ require 'openssl'
3
+ require 'uri'
4
+
5
+ require 'puppetserver/ca/errors'
6
+
7
+ module Puppetserver
8
+ module Ca
9
+ module Utils
10
+ # Utilities for doing HTTPS against the CA that wraps Net::HTTP constructs
11
+ class HttpClient
12
+
13
+ attr_reader :store
14
+
15
+ # Not all connections require a client cert to be present.
16
+ # For example, when querying the status endpoint.
17
+ def initialize(logger, settings, with_client_cert: true)
18
+ @default_headers = make_headers(ENV['HOME'])
19
+ @logger = logger
20
+ @store = make_store(settings[:localcacert],
21
+ settings[:certificate_revocation],
22
+ settings[:hostcrl])
23
+
24
+ if with_client_cert
25
+ @cert = load_cert(settings[:hostcert])
26
+ @key = load_key(settings[:hostprivkey])
27
+ else
28
+ @cert = nil
29
+ @key = nil
30
+ end
31
+ end
32
+
33
+ def load_cert(path)
34
+ load_with_errors(path, 'hostcert') do |content|
35
+ OpenSSL::X509::Certificate.new(content)
36
+ end
37
+ end
38
+
39
+ def load_key(path)
40
+ load_with_errors(path, 'hostprivkey') do |content|
41
+ OpenSSL::PKey.read(content)
42
+ end
43
+ end
44
+
45
+ # Takes an instance URL (defined lower in the file), and creates a
46
+ # connection. The given block is passed our own Connection object.
47
+ # The Connection object should have HTTP verbs defined on it that take
48
+ # a body (and optional overrides). Returns whatever the block given returned.
49
+ def with_connection(url, &block)
50
+ request = ->(conn) { block.call(Connection.new(conn, url, @logger, @default_headers)) }
51
+
52
+ begin
53
+ Net::HTTP.start(url.host, url.port,
54
+ use_ssl: true, cert_store: @store,
55
+ cert: @cert, key: @key,
56
+ &request)
57
+ rescue StandardError => e
58
+ raise ConnectionFailed.create(e,
59
+ "Failed connecting to #{url.full_url}\n" +
60
+ " Root cause: #{e.message}")
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def make_headers(home)
67
+ headers = {
68
+ 'User-Agent' => 'PuppetserverCaCli',
69
+ 'Content-Type' => 'application/json',
70
+ 'Accept' => 'application/json'
71
+ }
72
+
73
+ token_path = "#{home}/.puppetlabs/token"
74
+ if File.exist?(token_path)
75
+ headers['X-Authentication'] = File.read(token_path)
76
+ end
77
+
78
+ headers
79
+ end
80
+
81
+ def load_with_errors(path, setting, &block)
82
+ begin
83
+ content = File.read(path)
84
+ block.call(content)
85
+ rescue Errno::ENOENT => e
86
+ raise FileNotFound.create(e,
87
+ "Could not find '#{setting}' at '#{path}'")
88
+
89
+ rescue OpenSSL::OpenSSLError => e
90
+ raise InvalidX509Object.create(e,
91
+ "Could not parse '#{setting}' at '#{path}'.\n" +
92
+ " OpenSSL returned: #{e.message}")
93
+ end
94
+ end
95
+
96
+ # Helper class that wraps a Net::HTTP connection, a HttpClient::URL
97
+ # and defines methods named after HTTP verbs that are called on the
98
+ # saved connection, returning a Result.
99
+ class Connection
100
+
101
+ def initialize(net_http_connection, url_struct, logger, default_headers)
102
+ @conn = net_http_connection
103
+ @url = url_struct
104
+ @logger = logger
105
+ @default_headers = default_headers
106
+ end
107
+
108
+ def get(url_overide = nil, header_overrides = {})
109
+ url = url_overide || @url
110
+ headers = @default_headers.merge(header_overrides)
111
+
112
+ @logger.debug("Making a GET request at #{url.full_url}")
113
+
114
+ request = Net::HTTP::Get.new(url.to_uri, headers)
115
+ result = @conn.request(request)
116
+ Result.new(result.code, result.body)
117
+
118
+ end
119
+
120
+ def put(body, url_override = nil, header_overrides = {})
121
+ url = url_override || @url
122
+ headers = @default_headers.merge(header_overrides)
123
+
124
+ @logger.debug("Making a PUT request at #{url.full_url}")
125
+
126
+ request = Net::HTTP::Put.new(url.to_uri, headers)
127
+ request.body = body
128
+ result = @conn.request(request)
129
+
130
+ Result.new(result.code, result.body)
131
+ end
132
+
133
+ def post(body, url_override = nil, header_overrides = {})
134
+ url = url_override || @url
135
+ headers = @default_headers.merge(header_overrides)
136
+
137
+ @logger.debug("Making a POST request at #{url.full_url}")
138
+
139
+ request = Net::HTTP::Post.new(url.to_uri, headers)
140
+ request.body = body
141
+ result = @conn.request(request)
142
+
143
+ Result.new(result.code, result.body)
144
+ end
145
+
146
+ def delete(url_override = nil, header_overrides = {})
147
+ url = url_override || @url
148
+ headers = @default_headers.merge(header_overrides)
149
+
150
+ @logger.debug("Making a DELETE request at #{url.full_url}")
151
+
152
+ result = @conn.request(Net::HTTP::Delete.new(url.to_uri, headers))
153
+
154
+ Result.new(result.code, result.body)
155
+ end
156
+ end
157
+
158
+ # Just provide the bits of Net::HTTPResponse we care about
159
+ Result = Struct.new(:code, :body)
160
+
161
+ # Like URI, but not... maybe of suspicious value
162
+ URL = Struct.new(:protocol, :host, :port,
163
+ :endpoint, :version,
164
+ :resource_type, :resource_name, :query) do
165
+ def full_url
166
+ url = protocol + '://' + host + ':' + port + '/' +
167
+ [endpoint, version, resource_type, resource_name].compact.join('/')
168
+
169
+ url = url + "?" + URI.encode_www_form(query) unless query.nil? || query.empty?
170
+ return url
171
+ end
172
+
173
+ def to_uri
174
+ URI(full_url)
175
+ end
176
+ end
177
+
178
+ def make_store(bundle, crl_usage, crls = nil)
179
+ store = OpenSSL::X509::Store.new
180
+ store.purpose = OpenSSL::X509::PURPOSE_ANY
181
+ store.add_file(bundle)
182
+
183
+ if crl_usage != :ignore
184
+
185
+ flags = OpenSSL::X509::V_FLAG_CRL_CHECK
186
+ if crl_usage == :chain
187
+ flags |= OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
188
+ end
189
+
190
+ store.flags = flags
191
+ delimiter = /-----BEGIN X509 CRL-----.*?-----END X509 CRL-----/m
192
+ File.read(crls).scan(delimiter).each do |crl|
193
+ store.add_crl(OpenSSL::X509::CRL.new(crl))
194
+ end
195
+ end
196
+
197
+ store
198
+ end
199
+
200
+ # Queries the simple status endpoint for the status of the CA service.
201
+ # Returns true if it receives back a response of "running", and false if
202
+ # no connection can be made, or a different response is received.
203
+ def self.check_server_online(settings, logger)
204
+ status_url = URL.new('https', settings[:ca_server], settings[:ca_port], 'status', 'v1', 'simple', 'ca')
205
+ begin
206
+ # Generating certs offline is necessary if the server cert has been destroyed
207
+ # or compromised. Since querying the status endpoint does not require a client cert, and
208
+ # we commonly won't have one, don't require one for creating the connection.
209
+ # Additionally, we want to ensure the server is stopped before migrating the CA dir to
210
+ # avoid issues with writing to the CA dir and moving it.
211
+ self.new(logger, settings, with_client_cert: false).with_connection(status_url) do |conn|
212
+ result = conn.get
213
+ if result.body == "running"
214
+ logger.err "Puppetserver service is running. Please stop it before attempting to run this command."
215
+ true
216
+ else
217
+ false
218
+ end
219
+ end
220
+ rescue Puppetserver::Ca::ConnectionFailed => e
221
+ if e.wrapped.is_a? Errno::ECONNREFUSED
222
+ return false
223
+ else
224
+ raise e
225
+ end
226
+ end
227
+ end
228
+
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,84 @@
1
+ require 'time'
2
+
3
+ module Puppetserver
4
+ module Ca
5
+ module Utils
6
+ module Inventory
7
+
8
+ # Note that the inventory file may have multiple entries for the same certname,
9
+ # so it should only provide the latest cert for the given certname.
10
+ def self.parse_inventory_file(path, logger)
11
+ unless File.exist?(path)
12
+ logger.err("Could not find inventory at #{path}")
13
+ return [{}, true]
14
+ end
15
+ inventory = {}
16
+ errored = false
17
+ File.readlines(path).each do |line|
18
+ # Shouldn't be any blank lines, but skip them if there are
19
+ next if line.strip.empty?
20
+
21
+ items = line.strip.split
22
+ if items.count != 4
23
+ logger.err("Invalid entry found in inventory.txt: #{line}")
24
+ errored = true
25
+ next
26
+ end
27
+ unless items[0].match(/^(?:0x)?[A-Fa-f0-9]+$/)
28
+ logger.err("Invalid serial found in inventory.txt line: #{line}")
29
+ errored = true
30
+ next
31
+ end
32
+ serial = items[0].hex
33
+ not_before = nil
34
+ not_after = nil
35
+ begin
36
+ not_before = Time.parse(items[1])
37
+ rescue ArgumentError
38
+ logger.err("Invalid not_before time found in inventory.txt line: #{line}")
39
+ errored = true
40
+ next
41
+ end
42
+ begin
43
+ not_after = Time.parse(items[2])
44
+ rescue ArgumentError
45
+ logger.err("Invalid not_after time found in inventory.txt line: #{line}")
46
+ errored = true
47
+ next
48
+ end
49
+ unless items[3].start_with?('/CN=')
50
+ logger.err("Invalid certname found in inventory.txt line: #{line}")
51
+ errored = true
52
+ next
53
+ end
54
+ certname = items[3][4..-1]
55
+
56
+ if !inventory.keys.include?(certname)
57
+ inventory[certname] = {
58
+ :serial => serial,
59
+ :old_serials => [],
60
+ :not_before => not_before,
61
+ :not_after => not_after,
62
+ }
63
+ else
64
+ if not_after >= inventory[certname][:not_after]
65
+ # This is a newer cert than the one we currently have recorded,
66
+ # so save the previous serial in :old_serials
67
+ inventory[certname][:old_serials] << inventory[certname][:serial]
68
+ inventory[certname][:serial] = serial
69
+ inventory[certname][:not_before] = not_before
70
+ inventory[certname][:not_after] = not_after
71
+ else
72
+ # This somehow is an older cert (shouldn't really be possible as we just append
73
+ # to the file with each new cert and we are reading it order)
74
+ inventory[certname][:old_serials] << serial
75
+ end
76
+ end
77
+ end
78
+ [inventory, errored]
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
@@ -0,0 +1,27 @@
1
+ module Puppetserver
2
+ module Ca
3
+ module Utils
4
+ class SigningDigest
5
+
6
+ attr_reader :errors, :digest
7
+
8
+ def initialize
9
+ @errors = []
10
+ if OpenSSL::Digest.const_defined?('SHA256')
11
+ @digest = OpenSSL::Digest::SHA256.new
12
+ elsif OpenSSL::Digest.const_defined?('SHA1')
13
+ @digest = OpenSSL::Digest::SHA1.new
14
+ elsif OpenSSL::Digest.const_defined?('SHA512')
15
+ @digest = OpenSSL::Digest::SHA512.new
16
+ elsif OpenSSL::Digest.const_defined?('SHA384')
17
+ @digest = OpenSSL::Digest::SHA384.new
18
+ elsif OpenSSL::Digest.const_defined?('SHA224')
19
+ @digest = OpenSSL::Digest::SHA224.new
20
+ else
21
+ @errors << "Error: No FIPS 140-2 compliant digest algorithm in OpenSSL::Digest"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ module Puppetserver
2
+ module Ca
3
+ VERSION = "3.0.0-rc1"
4
+ end
5
+ end
@@ -0,0 +1,170 @@
1
+ require 'openssl'
2
+
3
+ module Puppetserver
4
+ module Ca
5
+ # Load, validate, and store x509 objects needed by the Puppet Server CA.
6
+ class X509Loader
7
+
8
+ attr_reader :errors, :certs, :cert, :key, :crls, :crl
9
+
10
+ def initialize(bundle_path, key_path, chain_path)
11
+ @errors = []
12
+
13
+ @certs = load_certs(bundle_path)
14
+ @key = load_key(key_path)
15
+ @crls = load_crls(chain_path)
16
+ @cert = find_signing_cert
17
+ @crl = find_leaf_crl
18
+
19
+ validate(@certs, @key, @crls)
20
+ end
21
+
22
+ def find_signing_cert
23
+ return if @key.nil? || @certs.empty?
24
+
25
+ signing_cert = @certs.find do |cert|
26
+ cert.check_private_key(@key)
27
+ end
28
+
29
+ if signing_cert.nil?
30
+ @errors << "Could not find certificate matching private key"
31
+ end
32
+
33
+ signing_cert
34
+ end
35
+
36
+ # Find a CRL in the chain issued by the signing cert
37
+ #
38
+ # @return [OpenSSL::X509::CRL] If a CRL is found.
39
+ # @return [nil] If no CRL is found.
40
+ def find_leaf_crl
41
+ return if @crls.empty? || @cert.nil?
42
+
43
+ leaf_crl = @crls.find do |crl|
44
+ crl.issuer == @cert.subject
45
+ end
46
+
47
+ leaf_crl
48
+ end
49
+
50
+ # Only do as much validation as is possible, assume whoever tried to
51
+ # load the objects wrote errors about any invalid ones, but that bundle
52
+ # and chain may be empty arrays and pkey may be nil.
53
+ def validate(bundle, pkey, chain)
54
+ if !@crl.nil? && !@cert.nil?
55
+ validate_crl_and_cert(@crl, @cert)
56
+ end
57
+
58
+ if pkey && !@cert.nil?
59
+ validate_cert_and_key(pkey, @cert)
60
+ end
61
+
62
+ unless bundle.empty? || @cert.nil? || @crl.nil?
63
+ validate_full_chain(bundle, chain)
64
+ end
65
+ end
66
+
67
+ def load_certs(bundle_path)
68
+ certs, errs = [], []
69
+
70
+ bundle_string = File.read(bundle_path)
71
+ cert_strings = bundle_string.scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m)
72
+ cert_strings.each do |cert_string|
73
+ begin
74
+ certs << OpenSSL::X509::Certificate.new(cert_string)
75
+ rescue OpenSSL::X509::CertificateError
76
+ errs << "Could not parse entry:\n#{cert_string}"
77
+ end
78
+ end
79
+
80
+ if certs.empty?
81
+ errs << "Could not detect any certs within #{bundle_path}"
82
+ end
83
+
84
+ unless errs.empty?
85
+ @errors << "Could not parse #{bundle_path}"
86
+ @errors += errs
87
+ end
88
+
89
+ return certs
90
+ end
91
+
92
+ def load_key(key_path)
93
+ begin
94
+ OpenSSL::PKey.read(File.read(key_path))
95
+ rescue ArgumentError, OpenSSL::PKey::PKeyError => e
96
+ @errors << "Could not parse #{key_path}"
97
+
98
+ return nil
99
+ end
100
+ end
101
+
102
+ def load_crls(chain_path)
103
+ errs, crls = [], []
104
+
105
+ chain_string = File.read(chain_path)
106
+ crl_strings = chain_string.scan(/-----BEGIN X509 CRL-----.*?-----END X509 CRL-----/m)
107
+ crl_strings.map do |crl_string|
108
+ begin
109
+ crls << OpenSSL::X509::CRL.new(crl_string)
110
+ rescue OpenSSL::X509::CRLError
111
+ errs << "Could not parse entry:\n#{crl_string}"
112
+ end
113
+ end
114
+
115
+ if crls.empty?
116
+ errs << "Could not detect any crls within #{chain_path}"
117
+ end
118
+
119
+ unless errs.empty?
120
+ @errors << "Could not parse #{chain_path}"
121
+ @errors += errs
122
+ end
123
+
124
+ return crls
125
+ end
126
+
127
+ # Replace the CRL for the signing cert of this loader
128
+ #
129
+ # @param new_crl [OpenSSL::X509::CRL]
130
+ # @return [void]
131
+ def crl=(new_crl)
132
+ @crl = new_crl
133
+ @crls = [new_crl] + @crls.reject {|c| c.issuer == new_crl.issuer }
134
+ end
135
+
136
+ def validate_cert_and_key(key, cert)
137
+ unless cert.check_private_key(key)
138
+ @errors << 'Private key and certificate do not match'
139
+ end
140
+ end
141
+
142
+ def validate_crl_and_cert(crl, cert)
143
+ unless crl.issuer == cert.subject
144
+ @errors << 'Leaf CRL was not issued by leaf certificate'
145
+ end
146
+ end
147
+
148
+ # By creating an X509::Store and validating the leaf cert with it we:
149
+ # - Ensure a full chain of trust (root to leaf) is within the bundle
150
+ # - If provided, there are CRLs for the CAs
151
+ # - If provided, no CAs within the chain of trust have been revoked
152
+ # However this does allow for:
153
+ # - Additional, ignored, certs and CRLs in the bundle/chain
154
+ # - certs and CRLs in any order
155
+ def validate_full_chain(certs, crls)
156
+ store = OpenSSL::X509::Store.new
157
+ certs.each {|cert| store.add_cert(cert) }
158
+ if !crls.empty?
159
+ store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
160
+ crls.each {|crl| store.add_crl(crl) }
161
+ end
162
+
163
+ unless store.verify(@cert)
164
+ @errors << 'Leaf certificate could not be validated'
165
+ @errors << "Validating cert store returned: #{store.error} - #{store.error_string}"
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,7 @@
1
+ require "puppetserver/ca/version"
2
+ require "puppetserver/ca/stub"
3
+
4
+ module Puppetserver
5
+ module Ca
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "puppetserver/ca/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "openvoxserver-ca"
8
+ spec.version = Puppetserver::Ca::VERSION
9
+ spec.authors = ["OpenVox Project"]
10
+ spec.email = ["openvox@voxpupuli.org"]
11
+ spec.license = "Apache-2.0"
12
+
13
+ spec.summary = %q{A simple CLI tool for interacting with OpenVox Server's Certificate Authority}
14
+ spec.homepage = "https://github.com/OpenVoxProject/openvoxserver-ca/"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_runtime_dependency "openfact", [">= 5.0.0", "< 6"]
24
+
25
+ spec.add_development_dependency "bundler", ">= 1.16"
26
+ spec.add_development_dependency "rake", ">= 12.3.3"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+
29
+ # openvoxserver 7 uses jruby 9.3 which is compatible with MRI ruby 2.6
30
+ spec.required_ruby_version = '>= 2.6.0'
31
+ end
data/tasks/spec.rake ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc 'Run rspec test in sequential order'
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ desc 'Run rspec test in random order'
10
+ RSpec::Core::RakeTask.new(:spec_random) do |t|
11
+ t.rspec_opts = '--order random'
12
+ end
13
+ rescue LoadError
14
+ puts 'Could not load rspec'
15
+ end
data/tasks/vox.rake ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :vox do
4
+ desc 'Update the version in preparation for a release'
5
+ task 'version:bump:full', [:version] do |_, args|
6
+ abort 'You must provide a tag.' if args[:version].nil? || args[:version].empty?
7
+ version = args[:version]
8
+ abort "#{version} does not appear to be a valid version string in x.y.z format" unless Gem::Version.correct?(version)
9
+
10
+ # Update lib/facter/version.rb and openvox.gemspec
11
+ puts "Setting version to #{version}"
12
+
13
+ data = File.read('lib/puppetserver/ca/version.rb')
14
+ new_data = data.sub(/VERSION = "\d+\.\d+\.\d+(\.rc\d+)?"/, %(VERSION = "#{version}"))
15
+ warn 'Failed to update version in lib/facter/version.rb' if data == new_data
16
+
17
+ File.write('lib/puppetserver/ca/version.rb', new_data)
18
+ end
19
+ end