dapp 0.12.8 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/bin/dapp +0 -4
  3. data/config/en/common.yml +1 -0
  4. data/config/en/net_status.yml +5 -0
  5. data/lib/dapp.rb +20 -8
  6. data/lib/dapp/cli.rb +2 -5
  7. data/lib/dapp/cli/command/base.rb +1 -5
  8. data/lib/dapp/dapp.rb +0 -22
  9. data/lib/dapp/dapp/shellout/streaming.rb +2 -2
  10. data/lib/dapp/deployment/cli/command/deployment.rb +1 -3
  11. data/lib/dapp/deployment/cli/command/deployment/apply.rb +1 -1
  12. data/lib/dapp/deployment/dapp/dapp.rb +0 -2
  13. data/lib/dapp/dimg/cli/command/base.rb +4 -0
  14. data/lib/dapp/dimg/config/directive/git_artifact_remote.rb +1 -3
  15. data/lib/dapp/dimg/dimg.rb +1 -0
  16. data/lib/dapp/dimg/dimg/path.rb +0 -6
  17. data/lib/dapp/helper/trivia.rb +4 -0
  18. data/lib/dapp/kube.rb +1 -0
  19. data/lib/dapp/kube/cli/cli.rb +1 -0
  20. data/lib/dapp/kube/cli/command/base.rb +14 -0
  21. data/lib/dapp/kube/cli/command/kube.rb +21 -0
  22. data/lib/dapp/kube/cli/command/kube/deploy.rb +30 -0
  23. data/lib/dapp/kube/cli/command/kube/dismiss.rb +21 -0
  24. data/lib/dapp/kube/cli/command/kube/secret_file_encrypt.rb +23 -0
  25. data/lib/dapp/kube/cli/command/kube/secret_generate.rb +13 -0
  26. data/lib/dapp/kube/cli/command/kube/secret_key_generate.rb +13 -0
  27. data/lib/dapp/kube/dapp/command/common.rb +29 -0
  28. data/lib/dapp/kube/dapp/command/deploy.rb +192 -0
  29. data/lib/dapp/kube/dapp/command/dismiss.rb +25 -0
  30. data/lib/dapp/kube/dapp/command/secret_file_encrypt.rb +22 -0
  31. data/lib/dapp/{deployment → kube}/dapp/command/secret_generate.rb +3 -3
  32. data/lib/dapp/{deployment → kube}/dapp/command/secret_key_generate.rb +2 -2
  33. data/lib/dapp/kube/dapp/dapp.rb +16 -0
  34. data/lib/dapp/kube/error/base.rb +7 -0
  35. data/lib/dapp/kube/error/command.rb +7 -0
  36. data/lib/dapp/kube/kubernetes.rb +191 -0
  37. data/lib/dapp/kube/secret.rb +93 -0
  38. data/lib/dapp/version.rb +1 -1
  39. metadata +23 -37
  40. data/lib/dapp/dapp/sentry.rb +0 -112
  41. data/lib/dapp/deployment/cli/command/deployment/secret_generate.rb +0 -13
  42. data/lib/dapp/deployment/cli/command/deployment/secret_key_generate.rb +0 -13
  43. data/lib/dapp/helper/url.rb +0 -23
@@ -0,0 +1,13 @@
1
+ module Dapp::Kube::CLI::Command
2
+ class Kube < ::Dapp::CLI
3
+ class SecretKeyGenerate < Base
4
+ banner <<BANNER.freeze
5
+ Usage:
6
+
7
+ dapp kube secret key generate
8
+
9
+ Options:
10
+ BANNER
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ module Dapp
2
+ module Kube
3
+ module Dapp
4
+ module Command
5
+ module Common
6
+ def kube_check_helm!
7
+ raise Error::Command, code: :helm_not_found if shellout('which helm').exitstatus == 1
8
+ end
9
+
10
+ def kube_release_name
11
+ "#{name}-#{kube_namespace}"
12
+ end
13
+
14
+ def kube_namespace
15
+ options[:namespace].tr('_', '-')
16
+ end
17
+
18
+ def secret
19
+ @secret ||= Secret.new(ENV['DAPP_SECRET_KEY']) if ENV.key?('DAPP_SECRET_KEY')
20
+ end
21
+
22
+ def kubernetes
23
+ @kubernetes ||= Kubernetes.new(namespace: kube_namespace)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,192 @@
1
+ module Dapp
2
+ module Kube
3
+ module Dapp
4
+ module Command
5
+ module Deploy
6
+ def kube_deploy
7
+ raise Error::Command, code: :helm_directory_not_exist unless kube_chart_path.exist?
8
+ kube_check_helm!
9
+ kube_check_helm_chart!
10
+
11
+ repo = option_repo
12
+ image_version = options[:image_version]
13
+ validate_repo_name!(repo)
14
+ validate_tag_name!(image_version)
15
+
16
+ begin
17
+ kube_copy_chart
18
+ kube_helm_decode_secrets
19
+ kube_generate_helm_chart_tpl
20
+
21
+ additional_values = [].tap do |options|
22
+ options << "--values #{kube_chart_path("values/#{kube_namespace}.yaml")}" if kube_chart_path("values/#{kube_namespace}.yaml").exist?
23
+ options << "--values #{kube_tmp_chart_secret_values_path.exist? ? kube_tmp_chart_secret_values_path : kube_chart_secret_values_path}" if kube_chart_secret_values_path.exist?
24
+ end
25
+
26
+ set_options = [].tap do |options|
27
+ options << "--set global.dapp.repo=#{repo}"
28
+ options << "--set global.dapp.image_version=#{image_version}"
29
+ options << "--set global.namespace=#{kube_namespace}"
30
+ options << "--set global.env=#{kube_namespace}"
31
+ end
32
+
33
+ kube_flush_hooks_jobs(additional_values, set_options)
34
+ kube_run_deploy(additional_values, set_options)
35
+ ensure
36
+ if dev_mode?
37
+ log_info "Temporary chart directory: #{kube_tmp_chart_path}"
38
+ else
39
+ FileUtils.rm_rf(kube_tmp_chart_path)
40
+ end
41
+ end
42
+ end
43
+
44
+ def kube_copy_chart
45
+ FileUtils.cp_r("#{kube_chart_path}/.", kube_tmp_chart_path)
46
+ end
47
+
48
+ def kube_helm_decode_secrets
49
+ if secret.nil?
50
+ log_warning(desc: { code: :dapp_secret_key_not_found }) if kube_chart_secret_values_path.file? || kube_chart_secret_path.directory?
51
+ else
52
+ kube_helm_decode_secret_values
53
+ kube_helm_decode_secret_files
54
+ end
55
+ end
56
+
57
+ def kube_helm_decode_secret_values
58
+ return unless kube_chart_secret_values_path.file?
59
+ decoded_data = kube_helm_decode_json(YAML::load(kube_chart_secret_values_path.read))
60
+ kube_tmp_chart_secret_values_path.write(decoded_data.to_yaml)
61
+ end
62
+
63
+ def kube_helm_decode_json(json)
64
+ decode_value = proc do |value|
65
+ case value
66
+ when Array then value.map { |v| decode_value.call(v) }
67
+ when Hash then kube_helm_decode_json(value)
68
+ else
69
+ secret.extract(value)
70
+ end
71
+ end
72
+ json.each { |k, v| json[k] = decode_value.call(v) }
73
+ end
74
+
75
+ def kube_helm_decode_secret_files
76
+ return unless kube_chart_secret_path.directory?
77
+ Dir.glob(kube_chart_secret_path.join('**/*')).each do |entry|
78
+ next unless File.file?(entry)
79
+ secret_relative_path = Pathname(entry).subpath_of(kube_chart_secret_path)
80
+ IO.binwrite(kube_tmp_chart_secret_path(secret_relative_path), secret.extract(IO.binread(entry)))
81
+ end
82
+ end
83
+
84
+ def kube_generate_helm_chart_tpl
85
+ secret_directory = secret.nil? ? 'secret' : kube_tmp_chart_secret_path.subpath_of(kube_tmp_chart_path)
86
+ cont = <<-EOF
87
+ {{/* vim: set filetype=mustache: */}}
88
+
89
+ {{- define "dapp_secret_file" -}}
90
+ {{- $relative_file_path := index . 0 -}}
91
+ {{- $context := index . 1 -}}
92
+ {{- $context.Files.Get (print "#{secret_directory}/" $relative_file_path) -}}
93
+ {{- end -}}
94
+ EOF
95
+ kube_tmp_chart_path('templates/dapp_secret_file.tpl').write(cont)
96
+ end
97
+
98
+ def kube_flush_hooks_jobs(additional_values, set_options)
99
+ return if (config_jobs_names = kube_helm_hooks_jobs_to_delete(additional_values, set_options).keys).empty?
100
+ kube_job_list.reject { |name| config_jobs_names.include? name }.each do
101
+ log_process("Delete hooks job `#{job_name}` for release #{kube_release_name} ", short: true) { kube_delete_job!(name) }
102
+ end
103
+ end
104
+
105
+ def kube_helm_hooks_jobs_to_delete(additional_values, set_options)
106
+ generator = proc do |text|
107
+ text.split(/# Source.*|---/).reject {|c| c.strip.empty? }.map {|c| YAML::load(c) }.reduce({}) do |objects, c|
108
+ objects[c['kind']] ||= {}
109
+ objects[c['kind']][(c['metadata'] || {})['name']] = c
110
+ objects
111
+ end
112
+ end
113
+
114
+ args = [kube_release_name, kube_tmp_chart_path, additional_values, set_options, kube_helm_extra_options(dry_run: true)].flatten
115
+ output = shellout!("helm upgrade #{args.join(' ')}", log_verbose: false).stdout
116
+
117
+ manifest_start_index = output.lines.index("MANIFEST:\n") + 1
118
+ hook_start_index = output.lines.index("HOOKS:\n") + 1
119
+ configs = generator.call(output.lines[hook_start_index..manifest_start_index-1].join)
120
+
121
+ (configs['Job'] || []).reject do |_, c|
122
+ c['metadata'] ||= {}
123
+ c['metadata']['annotations'] ||= {}
124
+ c['metadata']['annotations']['helm.sh/resource-policy'] == 'keep'
125
+ end
126
+ end
127
+
128
+ def kube_job_list
129
+ kubernetes.job_list['items'].map { |i| i['metadata']['name'] }
130
+ end
131
+
132
+ def kube_delete_job!(name)
133
+ kubernetes.delete_job!(name)
134
+ loop do
135
+ break unless kubernetes.job?(name)
136
+ sleep 1
137
+ end
138
+ end
139
+
140
+ def kube_run_deploy(additional_values, set_options)
141
+ log_process("Deploy release #{kube_release_name} ", verbose: true) do
142
+ args = [kube_release_name, kube_tmp_chart_path, additional_values, set_options, kube_helm_extra_options].flatten
143
+ kubernetes.create_namespace!(kube_namespace) unless kubernetes.namespace?(kube_namespace)
144
+ shellout! "helm upgrade #{args.join(' ')}", force_log: true
145
+ end
146
+ end
147
+
148
+ def kube_check_helm_chart!
149
+ raise Error::Command, code: :project_helm_chart_not_found, data: { path: kube_chart_path } unless kube_chart_path.exist?
150
+ end
151
+
152
+ def kube_helm_extra_options(dry_run: dry_run?)
153
+ [].tap do |options|
154
+ options << "--namespace #{kube_namespace}"
155
+ options << '--install'
156
+ options << '--wait'
157
+ options << '--timeout 1800'
158
+
159
+ options << '--dry-run' if dry_run
160
+ options << '--debug' if dry_run || log_verbose?
161
+ end
162
+ end
163
+
164
+ def kube_tmp_chart_secret_values_path
165
+ kube_tmp_chart_path('decoded-secret-values.yaml')
166
+ end
167
+
168
+ def kube_tmp_chart_secret_path(*path)
169
+ kube_tmp_chart_path('decoded-secret', *path).tap { |p| p.parent.mkpath }
170
+ end
171
+
172
+ def kube_tmp_chart_path(*path)
173
+ @kube_tmp_path ||= Dir.mktmpdir('dapp-secret-', options[:tmp_dir_prefix] || '/tmp')
174
+ make_path(@kube_tmp_path, *path).expand_path.tap { |p| p.parent.mkpath }
175
+ end
176
+
177
+ def kube_chart_secret_values_path
178
+ kube_chart_path('secret-values.yaml').expand_path
179
+ end
180
+
181
+ def kube_chart_secret_path(*path)
182
+ kube_chart_path('secret', *path).expand_path
183
+ end
184
+
185
+ def kube_chart_path(*path)
186
+ make_path(self.path, '.helm', *path).expand_path
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,25 @@
1
+ module Dapp
2
+ module Kube
3
+ module Dapp
4
+ module Command
5
+ module Dismiss
6
+ def kube_dismiss
7
+ kube_check_helm!
8
+ kube_check_helm_release!
9
+ log_process("Delete release #{kube_release_name} ", verbose: true) do
10
+ with_log_indent do
11
+ shellout! "helm delete #{kube_release_name} --purge"
12
+ kubernetes.delete_namespace!(kube_namespace) if options[:with_namespace]
13
+ end
14
+ end
15
+ end
16
+
17
+ def kube_check_helm_release!
18
+ pr = shellout("helm list | grep #{kube_release_name}")
19
+ raise Error::Command, code: :helm_release_not_exist, data: { name: kube_release_name } if pr.status == 1 || pr.stdout.empty?
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ module Dapp
2
+ module Kube
3
+ module Dapp
4
+ module Command
5
+ module SecretFileEncrypt
6
+ def kube_secret_file_encrypt(file_path)
7
+ raise Error::Command, code: :secret_key_not_found if secret.nil?
8
+ raise Error::Command, code: :file_not_exist, data: { path: File.expand_path(file_path) } unless File.exist?(file_path)
9
+
10
+ encrypted_data = secret.generate(IO.binread(file_path))
11
+ if (output_file_path = options[:output_file_path]).nil?
12
+ puts encrypted_data
13
+ else
14
+ FileUtils.mkpath File.dirname(output_file_path)
15
+ IO.binwrite(output_file_path, encrypted_data)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,11 +1,11 @@
1
1
  module Dapp
2
- module Deployment
2
+ module Kube
3
3
  module Dapp
4
4
  module Command
5
5
  module SecretGenerate
6
- def deployment_secret_generate
6
+ def kube_secret_generate
7
7
  raise Error::Command, code: :secret_key_not_found if secret.nil?
8
- unless (data = $stdin.gets(nil)).nil?
8
+ unless (data = $stdin.noecho { |s| s.gets(nil) }).nil?
9
9
  puts unless data.end_with?("\n")
10
10
  puts secret.generate(data)
11
11
  end
@@ -1,9 +1,9 @@
1
1
  module Dapp
2
- module Deployment
2
+ module Kube
3
3
  module Dapp
4
4
  module Command
5
5
  module SecretKeyGenerate
6
- def deployment_secret_key_generate
6
+ def kube_secret_key_generate
7
7
  puts Secret.generate_key
8
8
  end
9
9
  end
@@ -0,0 +1,16 @@
1
+ module Dapp
2
+ module Kube
3
+ module Dapp
4
+ module Dapp
5
+ include Command::Deploy
6
+ include Command::Dismiss
7
+ include Command::SecretGenerate
8
+ include Command::SecretKeyGenerate
9
+ include Command::SecretFileEncrypt
10
+ include Command::Common
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ ::Dapp::Dapp.send(:include, ::Dapp::Kube::Dapp::Dapp)
@@ -0,0 +1,7 @@
1
+ module Dapp
2
+ module Kube
3
+ module Error
4
+ class Base < ::Dapp::Error::Base; end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Dapp
2
+ module Kube
3
+ module Error
4
+ class Command < Base; end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,191 @@
1
+ module Dapp
2
+ module Kube
3
+ class Kubernetes
4
+ def initialize(namespace: nil)
5
+ @namespace = namespace
6
+ @query_parameters = {}
7
+ end
8
+
9
+ def namespace
10
+ @namespace || 'default'
11
+ end
12
+
13
+ # Чтобы не перегружать методы явной передачей namespace.
14
+ # Данный метод может пригодиться только в ситуации, когда надо указать другой namespace,
15
+ # в большинстве случаев используется namespace из конструктора.
16
+ def with_namespace(namespace, &blk)
17
+ old_namespace = @namespace
18
+ begin
19
+ @namespace = namespace
20
+ return yield
21
+ ensure
22
+ @namespace = old_namespace
23
+ end
24
+ end
25
+
26
+ def with_query(query, &blk)
27
+ old_query = @query_parameters
28
+ begin
29
+ @query_parameters = query
30
+ return yield
31
+ ensure
32
+ @query_parameters = old_query
33
+ end
34
+ end
35
+
36
+ # NOTICE: Название метода аналогично kind'у выдаваемого результата.
37
+ # NOTICE: В данном случае в результате kind=DeploymentList.
38
+ # NOTICE: Методы создания/обновления/удаления сущностей kubernetes заканчиваются на '!'. Например, create_deployment!.
39
+
40
+ {
41
+ '/api/v1' => [:service, :replicationcontroller, :pod],
42
+ '/apis/extensions/v1beta1' => [:deployment, :replicaset],
43
+ '/apis/batch/v1' => [:job]
44
+ }.each do |api, objects|
45
+ objects.each do |object|
46
+ define_method :"#{object}_list" do |**query_parameters|
47
+ request!(:get, "#{api}/namespaces/#{namespace}/#{object}s", **query_parameters)
48
+ end
49
+
50
+ define_method object do |name, **query_parameters|
51
+ request!(:get, "#{api}/namespaces/#{namespace}/#{object}s/#{name}", **query_parameters)
52
+ end
53
+
54
+ define_method "#{object}_status" do |name, **query_parameters|
55
+ request!(:get, "#{api}/namespaces/#{namespace}/#{object}s/#{name}/status", **query_parameters)
56
+ end
57
+
58
+ define_method :"create_#{object}!" do |spec, **query_parameters|
59
+ request!(:post, "#{api}/namespaces/#{namespace}/#{object}s", body: spec, **query_parameters)
60
+ end
61
+
62
+ define_method :"replace_#{object}!" do |name, spec, **query_parameters|
63
+ request!(:put, "#{api}/namespaces/#{namespace}/#{object}s/#{name}", body: spec, **query_parameters)
64
+ end
65
+
66
+ define_method :"delete_#{object}!" do |name, **query_parameters|
67
+ request!(:delete, "#{api}/namespaces/#{namespace}/#{object}s/#{name}", **query_parameters)
68
+ end
69
+
70
+ define_method :"delete_#{object}s!" do |**query_parameters|
71
+ request!(:delete, "#{api}/namespaces/#{namespace}/#{object}s", **query_parameters)
72
+ end
73
+
74
+ define_method :"#{object}?" do |name, **query_parameters|
75
+ public_send(:"#{object}_list", **query_parameters)['items'].map { |item| item['metadata']['name'] }.include?(name)
76
+ end
77
+ end
78
+ end
79
+
80
+ def namespace_list(**query_parameters)
81
+ request!(:get, '/api/v1/namespaces', **query_parameters)
82
+ end
83
+
84
+ def namespace?(name, **query_parameters)
85
+ namespace_list(**query_parameters)['items'].map { |item| item['metadata']['name'] }.include?(name)
86
+ end
87
+
88
+ def create_namespace!(name, **query_parameters)
89
+ request!(:post, '/api/v1/namespaces', body: { metadata: { name: name } }, **query_parameters)
90
+ end
91
+
92
+ def delete_namespace!(name, **query_parameters)
93
+ request!(:delete, "/api/v1/namespaces/#{name}", **query_parameters)
94
+ end
95
+
96
+ def pod_log(name, follow: false, **query_parameters, &blk)
97
+ excon_parameters = follow ? { response_block: blk } : {}
98
+ request!(:get,
99
+ "/api/v1/namespaces/#{namespace}/pods/#{name}/log",
100
+ excon_parameters: excon_parameters,
101
+ **{ follow: follow }.merge(query_parameters))
102
+ rescue Excon::Error::Timeout
103
+ raise Error::Timeout
104
+ end
105
+
106
+ def event_list(**query_parameters)
107
+ request!(:get, "/api/v1/namespaces/#{namespace}/events", **query_parameters)
108
+ end
109
+
110
+ protected
111
+
112
+ # query_parameters — соответствует 'Query Parameters' в документации kubernetes
113
+ # excon_parameters — соответствует опциям Excon
114
+ # body — hash для http-body, соответствует 'Body Parameters' в документации kubernetes, опционален
115
+ def request!(method, path, body: nil, excon_parameters: {}, **query_parameters)
116
+ with_connection(excon_parameters: excon_parameters) do |conn|
117
+ request_parameters = {method: method, path: path, query: @query_parameters.merge(query_parameters)}
118
+ request_parameters[:body] = JSON.dump(body) if body
119
+ load_body! conn.request(request_parameters), request_parameters
120
+ end
121
+ end
122
+
123
+ def load_body!(response, request_parameters)
124
+ response_ok = response.status.to_s.start_with? '2'
125
+
126
+ if response_ok
127
+ JSON.parse(response.body)
128
+ else
129
+ err_data = {}
130
+ err_data[:response_http_status] = response.status
131
+ if response_body = (JSON.parse(response.body) rescue nil)
132
+ err_data[:response_body] = response_body
133
+ else
134
+ err_data[:response_raw_body] = response.body
135
+ end
136
+ err_data[:request_parameters] = request_parameters
137
+
138
+ if response.status.to_s.start_with? '5'
139
+ raise Error::Base, code: :server_error, data: err_data
140
+ elsif response.status.to_s == '404'
141
+ raise Error::NotFound, data: err_data
142
+ else not response.status.to_s.start_with? '2'
143
+ raise Error::Base, code: :bad_request, data: err_data
144
+ end
145
+ end
146
+ end
147
+
148
+ def with_connection(excon_parameters: {}, &blk)
149
+ old_ssl_ca_file = Excon.defaults[:ssl_ca_file]
150
+ old_middlewares = Excon.defaults[:middlewares].dup
151
+
152
+ begin
153
+ Excon.defaults[:ssl_ca_file] = kube_config.fetch('clusters', [{}]).first.fetch('cluster', {}).fetch('certificate-authority', nil)
154
+ Excon.defaults[:middlewares] << Excon::Middleware::RedirectFollower
155
+
156
+ return yield Excon.new(kube_server_url, **kube_server_options(excon_parameters))
157
+ ensure
158
+ Excon.defaults[:ssl_ca_file] = old_ssl_ca_file
159
+ Excon.defaults[:middlewares] = old_middlewares
160
+ end
161
+ end
162
+
163
+ def kube_server_url
164
+ @kube_server_url ||= begin
165
+ kube_config.fetch('clusters', [{}]).first.fetch('cluster', {}).fetch('server', nil).tap do |url|
166
+ begin
167
+ Excon.new(url, **kube_server_options).get
168
+ rescue Excon::Error::Socket
169
+ raise Error::ConnectionRefused, code: :kube_server_connection_refused, data: { url: url }
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ def kube_server_options(excon_parameters = {})
176
+ { client_cert: kube_config.fetch('users', [{}]).first.fetch('user', {}).fetch('client-certificate', nil),
177
+ client_key: kube_config.fetch('users', [{}]).first.fetch('user', {}).fetch('client-key', nil) }.merge(excon_parameters)
178
+ end
179
+
180
+ def kube_config
181
+ @kube_config ||= begin
182
+ if File.exist?((kube_config_path = File.join(ENV['HOME'], '.kube/config')))
183
+ YAML.load_file(kube_config_path)
184
+ else
185
+ raise Error::Base, code: :kube_config_not_found, data: { path: kube_config_path }
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end