google-ssl-cert 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +6 -0
- data/Guardfile +19 -0
- data/LICENSE.txt +1 -0
- data/README.md +153 -0
- data/Rakefile +14 -0
- data/docs/google-secrets-cheatsheet.md +6 -0
- data/exe/google-ssl-cert +14 -0
- data/google-ssl-cert.gemspec +43 -0
- data/lib/google-ssl-cert.rb +1 -0
- data/lib/google_ssl_cert/autoloader.rb +22 -0
- data/lib/google_ssl_cert/base.rb +14 -0
- data/lib/google_ssl_cert/cert.rb +85 -0
- data/lib/google_ssl_cert/cli/base.rb +12 -0
- data/lib/google_ssl_cert/cli/create.rb +67 -0
- data/lib/google_ssl_cert/cli/help/completion.md +20 -0
- data/lib/google_ssl_cert/cli/help/completion_script.md +3 -0
- data/lib/google_ssl_cert/cli/help/create.md +31 -0
- data/lib/google_ssl_cert/cli/help/prune.md +30 -0
- data/lib/google_ssl_cert/cli/help/secret/get.md +4 -0
- data/lib/google_ssl_cert/cli/help/secret/save.md +4 -0
- data/lib/google_ssl_cert/cli/help.rb +11 -0
- data/lib/google_ssl_cert/cli/prune.rb +88 -0
- data/lib/google_ssl_cert/cli/secret.rb +15 -0
- data/lib/google_ssl_cert/cli.rb +62 -0
- data/lib/google_ssl_cert/command.rb +89 -0
- data/lib/google_ssl_cert/completer/script.rb +8 -0
- data/lib/google_ssl_cert/completer/script.sh +10 -0
- data/lib/google_ssl_cert/completer.rb +159 -0
- data/lib/google_ssl_cert/global.rb +12 -0
- data/lib/google_ssl_cert/google_services.rb +30 -0
- data/lib/google_ssl_cert/helpers/global.rb +7 -0
- data/lib/google_ssl_cert/helpers/project_number.rb +15 -0
- data/lib/google_ssl_cert/logger.rb +28 -0
- data/lib/google_ssl_cert/logging.rb +9 -0
- data/lib/google_ssl_cert/name.rb +19 -0
- data/lib/google_ssl_cert/secret.rb +86 -0
- data/lib/google_ssl_cert/version.rb +3 -0
- data/lib/google_ssl_cert.rb +13 -0
- data/spec/cli_spec.rb +26 -0
- data/spec/spec_helper.rb +29 -0
- metadata +272 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
class GoogleSslCert::CLI
|
2
|
+
class Prune < Base
|
3
|
+
include GoogleSslCert::Helpers::ProjectNumber
|
4
|
+
|
5
|
+
def run
|
6
|
+
keep = @options[:keep] || 1
|
7
|
+
right = -1 - keep
|
8
|
+
certs = ssl_certs[0..right] || [] # delete all except the last cert
|
9
|
+
|
10
|
+
if certs.empty?
|
11
|
+
logger.info "No timestamped certs to prune with cert name: #{cert_base_name}"
|
12
|
+
return
|
13
|
+
end
|
14
|
+
|
15
|
+
preview_delete(certs) unless @options[:yes]
|
16
|
+
sure?
|
17
|
+
perform_delete(certs)
|
18
|
+
end
|
19
|
+
|
20
|
+
def preview_delete(certs)
|
21
|
+
logger.info "Will delete the following #{type} certs:"
|
22
|
+
certs.each do |cert|
|
23
|
+
logger.info " #{cert.name}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def perform_delete(certs)
|
28
|
+
certs.each do |cert|
|
29
|
+
delete(cert)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete(cert)
|
34
|
+
options = base_options.merge(ssl_certificate: cert.name)
|
35
|
+
ssl_service.delete(options)
|
36
|
+
logger.info "Deleted #{type} cert: #{cert.name}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def type
|
40
|
+
global? ? "global" : "region"
|
41
|
+
end
|
42
|
+
|
43
|
+
# sadly the filter option doesnt support globs or regexp so will have to filter with ruby
|
44
|
+
def ssl_certs
|
45
|
+
resp = ssl_service.list(base_options)
|
46
|
+
resp.select do |ssl|
|
47
|
+
match?(ssl.name)
|
48
|
+
end.sort_by(&:name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def match?(name)
|
52
|
+
!!(name =~ Regexp.new("^#{cert_base_name}-\\d{14}$"))
|
53
|
+
end
|
54
|
+
|
55
|
+
def cert_base_name
|
56
|
+
@cert_base_name = GoogleSslCert::Name.new(@options).base_name
|
57
|
+
end
|
58
|
+
|
59
|
+
def ssl_service
|
60
|
+
if global?
|
61
|
+
ssl_certificates
|
62
|
+
else
|
63
|
+
region_ssl_certificates
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def base_options
|
68
|
+
options = { project: ENV['GOOGLE_PROJECT'] }
|
69
|
+
options[:region] = ENV['GOOGLE_REGION'] unless global?
|
70
|
+
options
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def sure?(message="Are you sure?")
|
75
|
+
if @options[:yes]
|
76
|
+
sure = 'y'
|
77
|
+
else
|
78
|
+
print "#{message} (y/N) "
|
79
|
+
sure = $stdin.gets
|
80
|
+
end
|
81
|
+
|
82
|
+
unless sure =~ /^y/
|
83
|
+
puts "Whew! Exiting."
|
84
|
+
exit 0
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class GoogleSslCert::CLI
|
2
|
+
class Secret < GoogleSslCert::Command
|
3
|
+
desc "save", "Save secret value"
|
4
|
+
long_desc Help.text("secret/save")
|
5
|
+
def save(name, value)
|
6
|
+
GoogleSslCert::Secret.new(options).save(name, value)
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "get", "Get secret value"
|
10
|
+
def get(name)
|
11
|
+
value = GoogleSslCert::Secret.new(options).get(name)
|
12
|
+
puts "Secret name: #{name} value #{value}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module GoogleSslCert
|
2
|
+
class CLI < Command
|
3
|
+
class_option :verbose, type: :boolean
|
4
|
+
class_option :noop, type: :boolean
|
5
|
+
|
6
|
+
secret_name_option = Proc.new do
|
7
|
+
option :secret_name, desc: "Secret name, conventionally matches the cert name"
|
8
|
+
end
|
9
|
+
global_option = Proc.new do
|
10
|
+
option :global, type: :boolean, default: true, desc: "Flag to create global vs region cert"
|
11
|
+
end
|
12
|
+
cert_name_option = Proc.new do
|
13
|
+
option :cert_name, desc: "Google SSL Cert name"
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "create", "Create Google SSL Certificate and save to Secrets Manager"
|
17
|
+
long_desc Help.text(:create)
|
18
|
+
option :private_key, desc: "private key path"
|
19
|
+
option :certificate, desc: "certificate path"
|
20
|
+
option :save_secret, type: :boolean, default: true, desc: "whether or not to save to Google Secrets Manager"
|
21
|
+
option :extra_certs, desc: "Additional certs to be added to the secret value"
|
22
|
+
option :timestamp, type: :boolean, default: true, desc: "Auto-append timestamp to cert name. Appending a timestamp allows auto-pruning also"
|
23
|
+
option :prune, type: :boolean, default: true, desc: "Auto-prune old certs based on timestamp"
|
24
|
+
cert_name_option.call
|
25
|
+
secret_name_option.call
|
26
|
+
global_option.call
|
27
|
+
def create
|
28
|
+
Create.new(options).run
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "prune", "prune Google SSL Certificate and save to Secrets Manager"
|
32
|
+
long_desc Help.text(:prune)
|
33
|
+
cert_name_option.call
|
34
|
+
global_option.call
|
35
|
+
option :yes, aliases: %w[y], type: :boolean, desc: "Skip 'are you sure' prompt"
|
36
|
+
option :keep, type: :numeric, default: 1, desc: "Number of certs to keep"
|
37
|
+
def prune
|
38
|
+
Prune.new(options).run
|
39
|
+
end
|
40
|
+
|
41
|
+
desc "secret SUBCOMMAND", "secret subcommands"
|
42
|
+
long_desc Help.text(:secret)
|
43
|
+
subcommand "secret", Secret
|
44
|
+
|
45
|
+
desc "completion *PARAMS", "Prints words for auto-completion."
|
46
|
+
long_desc Help.text(:completion)
|
47
|
+
def completion(*params)
|
48
|
+
Completer.new(CLI, *params).run
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "completion_script", "Generates a script that can be eval to setup auto-completion."
|
52
|
+
long_desc Help.text(:completion_script)
|
53
|
+
def completion_script
|
54
|
+
Completer::Script.generate
|
55
|
+
end
|
56
|
+
|
57
|
+
desc "version", "prints version"
|
58
|
+
def version
|
59
|
+
puts VERSION
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
# Override thor's long_desc identation behavior
|
4
|
+
# https://github.com/erikhuda/thor/issues/398
|
5
|
+
class Thor
|
6
|
+
module Shell
|
7
|
+
class Basic
|
8
|
+
def print_wrapped(message, options = {})
|
9
|
+
message = "\n#{message}" unless message[0] == "\n"
|
10
|
+
stdout.puts message
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module GoogleSslCert
|
17
|
+
class Command < Thor
|
18
|
+
class << self
|
19
|
+
def dispatch(m, args, options, config)
|
20
|
+
# Allow calling for help via:
|
21
|
+
# google-ssl-cert command help
|
22
|
+
# google-ssl-cert command -h
|
23
|
+
# google-ssl-cert command --help
|
24
|
+
# google-ssl-cert command -D
|
25
|
+
#
|
26
|
+
# as well thor's normal way:
|
27
|
+
#
|
28
|
+
# google-ssl-cert help command
|
29
|
+
help_flags = Thor::HELP_MAPPINGS + ["help"]
|
30
|
+
if args.length > 1 && !(args & help_flags).empty?
|
31
|
+
args -= help_flags
|
32
|
+
args.insert(-2, "help")
|
33
|
+
end
|
34
|
+
|
35
|
+
# google-ssl-cert version
|
36
|
+
# google-ssl-cert --version
|
37
|
+
# google-ssl-cert -v
|
38
|
+
version_flags = ["--version", "-v"]
|
39
|
+
if args.length == 1 && !(args & version_flags).empty?
|
40
|
+
args = ["version"]
|
41
|
+
end
|
42
|
+
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
# Override command_help to include the description at the top of the
|
47
|
+
# long_description.
|
48
|
+
def command_help(shell, command_name)
|
49
|
+
meth = normalize_command_name(command_name)
|
50
|
+
command = all_commands[meth]
|
51
|
+
alter_command_description(command)
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
55
|
+
def alter_command_description(command)
|
56
|
+
return unless command
|
57
|
+
|
58
|
+
# Add description to beginning of long_description
|
59
|
+
long_desc = if command.long_description
|
60
|
+
"#{command.description}\n\n#{command.long_description}"
|
61
|
+
else
|
62
|
+
command.description
|
63
|
+
end
|
64
|
+
|
65
|
+
# add reference url to end of the long_description
|
66
|
+
unless website.empty?
|
67
|
+
full_command = [command.ancestor_name, command.name].compact.join('-')
|
68
|
+
url = "#{website}/reference/google-ssl-cert-#{full_command}"
|
69
|
+
long_desc += "\n\nHelp also available at: #{url}"
|
70
|
+
end
|
71
|
+
|
72
|
+
command.long_description = long_desc
|
73
|
+
end
|
74
|
+
private :alter_command_description
|
75
|
+
|
76
|
+
# meant to be overriden
|
77
|
+
def website
|
78
|
+
""
|
79
|
+
end
|
80
|
+
|
81
|
+
# https://github.com/erikhuda/thor/issues/244
|
82
|
+
# Deprecation warning: Thor exit with status 0 on errors. To keep this behavior, you must define `exit_on_failure?` in `Lono::CLI`
|
83
|
+
# You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.
|
84
|
+
def exit_on_failure?
|
85
|
+
true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
_google-ssl-cert() {
|
2
|
+
COMPREPLY=()
|
3
|
+
local word="${COMP_WORDS[COMP_CWORD]}"
|
4
|
+
local words=("${COMP_WORDS[@]}")
|
5
|
+
unset words[0]
|
6
|
+
local completion=$(google-ssl-cert completion ${words[@]})
|
7
|
+
COMPREPLY=( $(compgen -W "$completion" -- "$word") )
|
8
|
+
}
|
9
|
+
|
10
|
+
complete -F _google-ssl-cert google-ssl-cert
|
@@ -0,0 +1,159 @@
|
|
1
|
+
=begin
|
2
|
+
Code Explanation:
|
3
|
+
|
4
|
+
There are 3 types of things to auto-complete:
|
5
|
+
|
6
|
+
1. command: the command itself
|
7
|
+
2. parameters: command parameters.
|
8
|
+
3. options: command options
|
9
|
+
|
10
|
+
Here's an example:
|
11
|
+
|
12
|
+
mycli hello name --from me
|
13
|
+
|
14
|
+
* command: hello
|
15
|
+
* parameters: name
|
16
|
+
* option: --from
|
17
|
+
|
18
|
+
When command parameters are done processing, the remaining completion words will be options. We can tell that the command params are completed based on the method arity.
|
19
|
+
|
20
|
+
## Arity
|
21
|
+
|
22
|
+
For example, say you had a method for a CLI command with the following form:
|
23
|
+
|
24
|
+
ufo scale service count --cluster development
|
25
|
+
|
26
|
+
It's equivalent ruby method:
|
27
|
+
|
28
|
+
scale(service, count) = has an arity of 2
|
29
|
+
|
30
|
+
So typing:
|
31
|
+
|
32
|
+
ufo scale service count [TAB] # there are 3 parameters including the "scale" command according to Thor's CLI processing.
|
33
|
+
|
34
|
+
So the completion should only show options, something like this:
|
35
|
+
|
36
|
+
--noop --verbose --cluster
|
37
|
+
|
38
|
+
## Splat Arguments
|
39
|
+
|
40
|
+
When the ruby method has a splat argument, it's arity is negative. Here are some example methods and their arities.
|
41
|
+
|
42
|
+
ship(service) = 1
|
43
|
+
scale(service, count) = 2
|
44
|
+
ships(*services) = -1
|
45
|
+
foo(example, *rest) = -2
|
46
|
+
|
47
|
+
Fortunately, negative and positive arity values are processed the same way. So we take simply take the absolute value of the arity and process it the same.
|
48
|
+
|
49
|
+
Here are some test cases, hit TAB after typing the command:
|
50
|
+
|
51
|
+
google-ssl-cert completion
|
52
|
+
google-ssl-cert completion hello
|
53
|
+
google-ssl-cert completion hello name
|
54
|
+
google-ssl-cert completion hello name --
|
55
|
+
google-ssl-cert completion hello name --noop
|
56
|
+
|
57
|
+
google-ssl-cert completion
|
58
|
+
google-ssl-cert completion sub:goodbye
|
59
|
+
google-ssl-cert completion sub:goodbye name
|
60
|
+
|
61
|
+
## Subcommands and Thor::Group Registered Commands
|
62
|
+
|
63
|
+
Sometimes the commands are not simple thor commands but are subcommands or Thor::Group commands. A good specific example is the ufo tool.
|
64
|
+
|
65
|
+
* regular command: ufo ship
|
66
|
+
* subcommand: ufo docker
|
67
|
+
* Thor::Group command: ufo init
|
68
|
+
|
69
|
+
Auto-completion accounts for each of these type of commands.
|
70
|
+
=end
|
71
|
+
module GoogleSslCert
|
72
|
+
class Completer
|
73
|
+
def initialize(command_class, *params)
|
74
|
+
@params = params
|
75
|
+
@current_command = @params[0]
|
76
|
+
@command_class = command_class # CLI initiall
|
77
|
+
end
|
78
|
+
|
79
|
+
def run
|
80
|
+
if subcommand?(@current_command)
|
81
|
+
subcommand_class = @command_class.subcommand_classes[@current_command]
|
82
|
+
@params.shift # destructive
|
83
|
+
Completer.new(subcommand_class, *@params).run # recursively use subcommand
|
84
|
+
return
|
85
|
+
end
|
86
|
+
|
87
|
+
# full command has been found!
|
88
|
+
unless found?(@current_command)
|
89
|
+
puts all_commands
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
# will only get to here if command aws found (above)
|
94
|
+
arity = @command_class.instance_method(@current_command).arity.abs
|
95
|
+
if @params.size > arity or thor_group_command?
|
96
|
+
puts options_completion
|
97
|
+
else
|
98
|
+
puts params_completion
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def subcommand?(command)
|
103
|
+
@command_class.subcommands.include?(command)
|
104
|
+
end
|
105
|
+
|
106
|
+
# hacky way to detect that command is a registered Thor::Group command
|
107
|
+
def thor_group_command?
|
108
|
+
command_params(raw=true) == [[:rest, :args]]
|
109
|
+
end
|
110
|
+
|
111
|
+
def found?(command)
|
112
|
+
public_methods = @command_class.public_instance_methods(false)
|
113
|
+
command && public_methods.include?(command.to_sym)
|
114
|
+
end
|
115
|
+
|
116
|
+
# all top-level commands
|
117
|
+
def all_commands
|
118
|
+
commands = @command_class.all_commands.reject do |k,v|
|
119
|
+
v.is_a?(Thor::HiddenCommand)
|
120
|
+
end
|
121
|
+
commands.keys
|
122
|
+
end
|
123
|
+
|
124
|
+
def command_params(raw=false)
|
125
|
+
params = @command_class.instance_method(@current_command).parameters
|
126
|
+
# Example:
|
127
|
+
# >> Sub.instance_method(:goodbye).parameters
|
128
|
+
# => [[:req, :name]]
|
129
|
+
# >>
|
130
|
+
raw ? params : params.map!(&:last)
|
131
|
+
end
|
132
|
+
|
133
|
+
def params_completion
|
134
|
+
offset = @params.size - 1
|
135
|
+
offset_params = command_params[offset..-1]
|
136
|
+
command_params[offset..-1].first
|
137
|
+
end
|
138
|
+
|
139
|
+
def options_completion
|
140
|
+
used = ARGV.select { |a| a.include?('--') } # so we can remove used options
|
141
|
+
|
142
|
+
method_options = @command_class.all_commands[@current_command].options.keys
|
143
|
+
class_options = @command_class.class_options.keys
|
144
|
+
|
145
|
+
all_options = method_options + class_options + ['help']
|
146
|
+
|
147
|
+
all_options.map! { |o| "--#{o.to_s.gsub('_','-')}" }
|
148
|
+
filtered_options = all_options - used
|
149
|
+
filtered_options.uniq
|
150
|
+
end
|
151
|
+
|
152
|
+
# Useful for debugging. Using puts messes up completion.
|
153
|
+
def log(msg)
|
154
|
+
File.open("/tmp/complete.log", "a") do |file|
|
155
|
+
file.puts(msg)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module GoogleSslCert
|
2
|
+
class Global
|
3
|
+
def initialize(options={})
|
4
|
+
@options = options
|
5
|
+
end
|
6
|
+
|
7
|
+
def global?
|
8
|
+
default_global = !%w[0 false].include?(ENV['GSC_GLOBAL']) # nil will default to true
|
9
|
+
@options[:global].nil? ? default_global : @options[:global]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "google-cloud-resource_manager"
|
2
|
+
require "google-cloud-secret_manager"
|
3
|
+
require "google/cloud/compute/v1/region_ssl_certificates"
|
4
|
+
require "google/cloud/compute/v1/ssl_certificates"
|
5
|
+
|
6
|
+
module GoogleSslCert
|
7
|
+
module GoogleServices
|
8
|
+
extend Memoist
|
9
|
+
|
10
|
+
def region_ssl_certificates
|
11
|
+
Google::Cloud::Compute::V1::RegionSslCertificates::Rest::Client.new
|
12
|
+
end
|
13
|
+
memoize :region_ssl_certificates
|
14
|
+
|
15
|
+
def secret_manager_service
|
16
|
+
Google::Cloud::SecretManager.secret_manager_service
|
17
|
+
end
|
18
|
+
memoize :secret_manager_service
|
19
|
+
|
20
|
+
def ssl_certificates
|
21
|
+
Google::Cloud::Compute::V1::SslCertificates::Rest::Client.new
|
22
|
+
end
|
23
|
+
memoize :ssl_certificates
|
24
|
+
|
25
|
+
def resource_manager
|
26
|
+
Google::Cloud.new.resource_manager
|
27
|
+
end
|
28
|
+
memoize :resource_manager
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module GoogleSslCert::Helpers
|
2
|
+
module ProjectNumber
|
3
|
+
private
|
4
|
+
def parent
|
5
|
+
"projects/#{project_number}"
|
6
|
+
end
|
7
|
+
|
8
|
+
@@project_number = nil
|
9
|
+
def project_number
|
10
|
+
return @@project_number if @@project_number
|
11
|
+
project = resource_manager.project(ENV['GOOGLE_PROJECT'])
|
12
|
+
@@project_number = project.project_number
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module GoogleSslCert
|
4
|
+
class Logger < ::Logger
|
5
|
+
def initialize(*args)
|
6
|
+
super
|
7
|
+
self.formatter = Formatter.new
|
8
|
+
self.level = log_level
|
9
|
+
end
|
10
|
+
|
11
|
+
def log_level
|
12
|
+
if ENV['DEBUG']
|
13
|
+
:debug
|
14
|
+
else
|
15
|
+
ENV['GSC_LOG_LEVEL'] || :info # note: only respected when config.logger not set in config/app.rb
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def format_message(severity, datetime, progname, msg)
|
20
|
+
line = if @logdev.dev == $stdout || @logdev.dev == $stderr
|
21
|
+
msg # super simple format if stdout
|
22
|
+
else
|
23
|
+
super # use the configured formatter
|
24
|
+
end
|
25
|
+
line =~ /\n$/ ? line : "#{line}\n"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module GoogleSslCert
|
2
|
+
class Name
|
3
|
+
attr_reader :base_name
|
4
|
+
def initialize(options={})
|
5
|
+
@options = options
|
6
|
+
@base_name = @options[:cert_name] || default_cert_name
|
7
|
+
end
|
8
|
+
|
9
|
+
def generate
|
10
|
+
ts = Time.now.strftime("%Y%m%d%H%M%S") unless @options[:timestamp] == false # nil defaults to true
|
11
|
+
[@base_name, ts].compact.join('-')
|
12
|
+
end
|
13
|
+
|
14
|
+
def default_cert_name
|
15
|
+
type = @options[:global] ? "global" : ENV['GOOGLE_REGION']
|
16
|
+
["google-ssl-cert", type].join('-')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module GoogleSslCert
|
2
|
+
class Secret < Base
|
3
|
+
extend Memoist
|
4
|
+
|
5
|
+
# CLI commands:
|
6
|
+
# gcloud secrets create testsecret
|
7
|
+
# gcloud secrets versions add testsecret --data-file="/tmp/testsecret.txt"
|
8
|
+
#
|
9
|
+
# Secret create API docs
|
10
|
+
# https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets#secretmanager-create-secret-ruby
|
11
|
+
# https://github.com/googleapis/google-cloud-ruby/blob/af60d07b8f134ebc35bee795d127be614abea353/google-cloud-secret_manager-v1/lib/google/cloud/secret_manager/v1/secret_manager_service/client.rb#L307
|
12
|
+
# https://cloud.google.com/secret-manager/docs/reference/rest/v1/projects.secrets/create
|
13
|
+
# Secret Versions add API docs
|
14
|
+
# https://github.com/googleapis/google-cloud-ruby/blob/af60d07b8f134ebc35bee795d127be614abea353/google-cloud-secret_manager-v1/lib/google/cloud/secret_manager/v1/secret_manager_service/client.rb#L379
|
15
|
+
# https://cloud.google.com/secret-manager/docs/reference/rest/v1/projects.secrets/addVersion
|
16
|
+
# https://cloud.google.com/secret-manager/docs/reference/rest/v1/SecretPayload
|
17
|
+
def save(name, value)
|
18
|
+
validate!
|
19
|
+
create_secret(name, value)
|
20
|
+
url_path = "#{parent}/secrets/#{name}"
|
21
|
+
secret_manager_service.add_secret_version(parent: url_path, payload: {data: value})
|
22
|
+
logger.info "Secret saved: name: #{name} value: #{value}"
|
23
|
+
rescue Google::Cloud::AlreadyExistsError => e
|
24
|
+
logger.error("#{e.class}: #{e.message}")
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_secret(name, value)
|
28
|
+
secret = get_secret(name)
|
29
|
+
return if secret
|
30
|
+
secret_manager_service.create_secret(
|
31
|
+
parent: parent,
|
32
|
+
secret_id: name,
|
33
|
+
secret: {
|
34
|
+
replication: {
|
35
|
+
automatic: {}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_secret(name)
|
42
|
+
url_path = "#{parent}/secrets/#{name}"
|
43
|
+
secret_manager_service.get_secret(name: url_path)
|
44
|
+
rescue Google::Cloud::NotFoundError
|
45
|
+
nil
|
46
|
+
rescue Google::Cloud::InvalidArgumentError => e
|
47
|
+
logger.fatal("ERROR: #{e.class}: #{e.message}\n")
|
48
|
+
logger.fatal("Expected format: [[a-zA-Z_0-9]+]")
|
49
|
+
exit 1
|
50
|
+
end
|
51
|
+
|
52
|
+
# CLI commands:
|
53
|
+
# gcloud secrets list
|
54
|
+
# gcloud secrets versions access latest --secret testsecret
|
55
|
+
#
|
56
|
+
# Secret access version API docs
|
57
|
+
# https://cloud.google.com/secret-manager/docs/reference/rest/v1/projects.secrets.versions/access
|
58
|
+
# https://cloud.google.com/secret-manager/docs/reference/rest/v1/SecretPayload
|
59
|
+
def get(name)
|
60
|
+
version = @options[:version] || "latest"
|
61
|
+
url_path = "#{parent}/secrets/#{name}/versions/#{version}"
|
62
|
+
version = secret_manager_service.access_secret_version(name: url_path)
|
63
|
+
version.payload.data
|
64
|
+
rescue Google::Cloud::NotFoundError => e
|
65
|
+
logger.error "WARN: secret #{name.color(:yellow)} not found"
|
66
|
+
logger.error e.message
|
67
|
+
"NOT FOUND #{name}" # simple string so Kubernetes YAML is valid
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate!
|
71
|
+
errors = []
|
72
|
+
secret_name = @options[:secret_name]
|
73
|
+
if @options[:save_secret] && !secret_name
|
74
|
+
errors << "ERROR: --secret-name must be provided or --no-save-secret option must be used"
|
75
|
+
end
|
76
|
+
# extra validation early to prevent google ssl cert from being created but the secret not being stored
|
77
|
+
if secret_name && secret_name !~ /^[a-zA-Z_\-0-9]+$/
|
78
|
+
errors << "ERROR: --secret-name invalid format. Expected format: [a-zA-Z_0-9]+" # Expected format taken from `gcloud secrets create`
|
79
|
+
end
|
80
|
+
unless errors.empty?
|
81
|
+
logger.error errors.join("\n")
|
82
|
+
exit 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
$stdout.sync = true unless ENV["GOOGLE_SSL_CERT_STDOUT_SYNC"] == "0"
|
2
|
+
|
3
|
+
$:.unshift(File.expand_path("../", __FILE__))
|
4
|
+
|
5
|
+
require "google_ssl_cert/autoloader"
|
6
|
+
GoogleSslCert::Autoloader.setup
|
7
|
+
|
8
|
+
require "memoist"
|
9
|
+
require "rainbow/ext/string"
|
10
|
+
|
11
|
+
module GoogleSslCert
|
12
|
+
class Error < StandardError; end
|
13
|
+
end
|