uffizzi-cli 2.2.0 → 2.2.2
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 +4 -4
- data/lib/uffizzi/auth_helper.rb +1 -1
- data/lib/uffizzi/cli/cluster.rb +57 -70
- data/lib/uffizzi/cli/dev.rb +128 -46
- data/lib/uffizzi/helpers/config_helper.rb +8 -0
- data/lib/uffizzi/services/cluster_service.rb +24 -0
- data/lib/uffizzi/services/dev_service.rb +144 -10
- data/lib/uffizzi/shell.rb +18 -0
- data/lib/uffizzi/version.rb +1 -1
- data/lib/uffizzi.rb +13 -1
- data/man/uffizzi +3 -0
- data/man/uffizzi-dev-describe +36 -0
- data/man/uffizzi-dev-describe.ronn +27 -0
- data/man/uffizzi.ronn +3 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f1280afea82284077769e8545880e7420aede5c114a837f3ae2e21482637bac
|
|
4
|
+
data.tar.gz: e9f5235cb793d83b710c291c62f5027f0226d400aa836f9ea56c18f95b474d06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca04830842f417cb76799ae64c1106866a4d91a7774a07fa3c63fbbe726eac5ab78381c2059ef23a6157948b457984e723bc0e4273d6dbeda48d406379ebe3e4
|
|
7
|
+
data.tar.gz: 119bb3460dc17116351d4553400867b6225e6431614bdb6934d5f4dc1c532c0cf21b7d61a254210ac2a0eb0643d7183a6ac301b29ea352d81f6dc6feb6a339f2
|
data/lib/uffizzi/auth_helper.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Uffizzi
|
|
|
16
16
|
Uffizzi::Token.delete if Uffizzi::Token.exists?
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def check_login(project_option)
|
|
19
|
+
def check_login(project_option = nil)
|
|
20
20
|
raise Uffizzi::Error.new('You are not logged in. Run `uffizzi login`.') unless signed_in?
|
|
21
21
|
raise Uffizzi::Error.new('This command needs project to be set in config file') unless project_set?(project_option)
|
|
22
22
|
end
|
data/lib/uffizzi/cli/cluster.rb
CHANGED
|
@@ -76,19 +76,18 @@ module Uffizzi
|
|
|
76
76
|
def run(command, command_args = {})
|
|
77
77
|
Uffizzi.ui.output_format = options[:output]
|
|
78
78
|
Uffizzi::AuthHelper.check_login(options[:project])
|
|
79
|
-
project_slug = options[:project].nil? ? ConfigFile.read_option(:project) : options[:project]
|
|
80
79
|
|
|
81
80
|
case command
|
|
82
81
|
when 'list'
|
|
83
|
-
handle_list_command
|
|
82
|
+
handle_list_command
|
|
84
83
|
when 'create'
|
|
85
|
-
handle_create_command(
|
|
84
|
+
handle_create_command(command_args)
|
|
86
85
|
when 'describe'
|
|
87
|
-
handle_describe_command(
|
|
86
|
+
handle_describe_command(command_args)
|
|
88
87
|
when 'delete'
|
|
89
|
-
handle_delete_command(
|
|
88
|
+
handle_delete_command(command_args)
|
|
90
89
|
when 'update-kubeconfig'
|
|
91
|
-
handle_update_kubeconfig_command(
|
|
90
|
+
handle_update_kubeconfig_command(command_args)
|
|
92
91
|
when 'disconnect'
|
|
93
92
|
ClusterDisconnectService.handle(options)
|
|
94
93
|
when 'sleep'
|
|
@@ -98,13 +97,12 @@ module Uffizzi
|
|
|
98
97
|
end
|
|
99
98
|
end
|
|
100
99
|
|
|
101
|
-
def handle_list_command
|
|
100
|
+
def handle_list_command
|
|
102
101
|
is_all = options[:all]
|
|
103
102
|
response = if is_all
|
|
104
|
-
get_account_clusters(
|
|
103
|
+
get_account_clusters(server, ConfigFile.read_option(:account, :id))
|
|
105
104
|
else
|
|
106
|
-
|
|
107
|
-
get_project_clusters(ConfigFile.read_option(:server), project_slug, oidc_token: oidc_token)
|
|
105
|
+
get_project_clusters(server, project_slug, oidc_token: oidc_token)
|
|
108
106
|
end
|
|
109
107
|
|
|
110
108
|
if ResponseHelper.ok?(response)
|
|
@@ -115,7 +113,7 @@ module Uffizzi
|
|
|
115
113
|
end
|
|
116
114
|
|
|
117
115
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
118
|
-
def handle_create_command(
|
|
116
|
+
def handle_create_command(command_args)
|
|
119
117
|
Uffizzi.ui.disable_stdout if Uffizzi.ui.output_format
|
|
120
118
|
|
|
121
119
|
if options[:name]
|
|
@@ -125,23 +123,20 @@ module Uffizzi
|
|
|
125
123
|
end
|
|
126
124
|
|
|
127
125
|
cluster_name = command_args[:name] || options[:name] || ClusterService.generate_name
|
|
128
|
-
creation_source = options[:"creation-source"] || ClusterService::MANUAL_CREATION_SOURCE
|
|
129
|
-
k8s_version = options[:"k8s-version"]
|
|
130
126
|
Uffizzi.ui.say_error_and_exit("Cluster name: #{cluster_name} is not valid.") unless ClusterService.valid_name?(cluster_name)
|
|
131
127
|
|
|
132
|
-
|
|
133
|
-
name: cluster_name
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
)
|
|
138
|
-
response = create_cluster(ConfigFile.read_option(:server), project_slug, params)
|
|
128
|
+
unless ClusterService.valid_name?(cluster_name)
|
|
129
|
+
Uffizzi.ui.say_error_and_exit("Cluster name: #{cluster_name} is not valid.")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
params = cluster_creation_params(cluster_name)
|
|
133
|
+
response = create_cluster(server, project_slug, params)
|
|
139
134
|
|
|
140
135
|
return ResponseHelper.handle_failed_response(response) unless ResponseHelper.created?(response)
|
|
141
136
|
|
|
142
137
|
spinner = TTY::Spinner.new("[:spinner] Creating cluster #{cluster_name}...", format: :dots)
|
|
143
138
|
spinner.auto_spin
|
|
144
|
-
cluster_data = ClusterService.wait_cluster_deploy(project_slug, cluster_name,
|
|
139
|
+
cluster_data = ClusterService.wait_cluster_deploy(project_slug, cluster_name, oidc_token)
|
|
145
140
|
|
|
146
141
|
if ClusterService.failed?(cluster_data[:state])
|
|
147
142
|
spinner.error
|
|
@@ -151,26 +146,28 @@ module Uffizzi
|
|
|
151
146
|
spinner.success
|
|
152
147
|
handle_succeed_create_response(cluster_data)
|
|
153
148
|
rescue SystemExit, Interrupt, SocketError
|
|
154
|
-
handle_interrupt_creation(cluster_name
|
|
149
|
+
handle_interrupt_creation(cluster_name)
|
|
155
150
|
end
|
|
156
151
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
157
152
|
|
|
158
|
-
def handle_describe_command(
|
|
159
|
-
cluster_data = fetch_cluster_data(
|
|
153
|
+
def handle_describe_command(command_args)
|
|
154
|
+
cluster_data = ClusterService.fetch_cluster_data(command_args[:cluster_name], **cluster_api_connection_params)
|
|
155
|
+
render_data = ClusterService.build_render_data(cluster_data)
|
|
160
156
|
|
|
161
|
-
|
|
157
|
+
Uffizzi.ui.output_format = Uffizzi::UI::Shell::PRETTY_LIST
|
|
158
|
+
Uffizzi.ui.say(render_data)
|
|
162
159
|
end
|
|
163
160
|
|
|
164
|
-
def handle_delete_command(
|
|
161
|
+
def handle_delete_command(command_args)
|
|
165
162
|
cluster_name = command_args[:cluster_name]
|
|
166
163
|
is_delete_kubeconfig = options[:'delete-config']
|
|
167
164
|
|
|
168
|
-
return handle_delete_cluster(
|
|
165
|
+
return handle_delete_cluster(cluster_name) unless is_delete_kubeconfig
|
|
169
166
|
|
|
170
|
-
cluster_data = fetch_cluster_data(
|
|
167
|
+
cluster_data = ClusterService.fetch_cluster_data(cluster_name, **cluster_api_connection_params)
|
|
171
168
|
kubeconfig = parse_kubeconfig(cluster_data[:kubeconfig])
|
|
172
169
|
|
|
173
|
-
handle_delete_cluster(
|
|
170
|
+
handle_delete_cluster(cluster_name)
|
|
174
171
|
exclude_kubeconfig(cluster_data[:id], kubeconfig) if kubeconfig.present?
|
|
175
172
|
end
|
|
176
173
|
|
|
@@ -194,12 +191,12 @@ module Uffizzi
|
|
|
194
191
|
end
|
|
195
192
|
end
|
|
196
193
|
|
|
197
|
-
def handle_delete_cluster(
|
|
194
|
+
def handle_delete_cluster(cluster_name)
|
|
198
195
|
params = {
|
|
199
196
|
cluster_name: cluster_name,
|
|
200
|
-
oidc_token:
|
|
197
|
+
oidc_token: oidc_token,
|
|
201
198
|
}
|
|
202
|
-
response = delete_cluster(
|
|
199
|
+
response = delete_cluster(server, project_slug, params)
|
|
203
200
|
|
|
204
201
|
if ResponseHelper.no_content?(response)
|
|
205
202
|
Uffizzi.ui.say("Cluster #{cluster_name} deleted")
|
|
@@ -208,10 +205,10 @@ module Uffizzi
|
|
|
208
205
|
end
|
|
209
206
|
end
|
|
210
207
|
|
|
211
|
-
def handle_update_kubeconfig_command(
|
|
208
|
+
def handle_update_kubeconfig_command(command_args)
|
|
212
209
|
kubeconfig_path = options[:kubeconfig] || KubeconfigService.default_path
|
|
213
210
|
cluster_name = command_args[:cluster_name]
|
|
214
|
-
cluster_data = fetch_cluster_data(
|
|
211
|
+
cluster_data = ClusterService.fetch_cluster_data(cluster_name, **cluster_api_connection_params)
|
|
215
212
|
|
|
216
213
|
unless cluster_data[:kubeconfig].present?
|
|
217
214
|
say_error_update_kubeconfig(cluster_data)
|
|
@@ -289,13 +286,15 @@ module Uffizzi
|
|
|
289
286
|
end
|
|
290
287
|
end
|
|
291
288
|
|
|
292
|
-
def cluster_creation_params(
|
|
289
|
+
def cluster_creation_params(cluster_name)
|
|
290
|
+
creation_source = options[:"creation-source"] || ClusterService::MANUAL_CREATION_SOURCE
|
|
291
|
+
manifest_file_path = options[:manifest]
|
|
292
|
+
k8s_version = options[:"k8s-version"]
|
|
293
293
|
manifest_content = load_manifest_file(manifest_file_path)
|
|
294
|
-
oidc_token = Uffizzi::ConfigFile.read_option(:oidc_token)
|
|
295
294
|
|
|
296
295
|
{
|
|
297
296
|
cluster: {
|
|
298
|
-
name:
|
|
297
|
+
name: cluster_name,
|
|
299
298
|
manifest: manifest_content,
|
|
300
299
|
creation_source: creation_source,
|
|
301
300
|
k8s_version: k8s_version,
|
|
@@ -312,7 +311,7 @@ module Uffizzi
|
|
|
312
311
|
raise Uffizzi::Error.new(e.message)
|
|
313
312
|
end
|
|
314
313
|
|
|
315
|
-
def handle_interrupt_creation(cluster_name
|
|
314
|
+
def handle_interrupt_creation(cluster_name)
|
|
316
315
|
deletion_response = delete_cluster(server, project_slug, cluster_name: cluster_name)
|
|
317
316
|
deletion_message = if ResponseHelper.no_content?(deletion_response)
|
|
318
317
|
"The cluster #{cluster_name} has been disabled."
|
|
@@ -348,24 +347,6 @@ module Uffizzi
|
|
|
348
347
|
end.join("\n")
|
|
349
348
|
end
|
|
350
349
|
|
|
351
|
-
def handle_succeed_describe(cluster_data)
|
|
352
|
-
prepared_cluster_data = {
|
|
353
|
-
name: cluster_data[:name],
|
|
354
|
-
status: cluster_data[:state],
|
|
355
|
-
created: Time.strptime(cluster_data[:created_at], '%Y-%m-%dT%H:%M:%S.%N').strftime('%a %b %d %H:%M:%S %Y'),
|
|
356
|
-
url: cluster_data[:host],
|
|
357
|
-
k8s_version: cluster_data[:k8s_version],
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
rendered_cluster_data = if Uffizzi.ui.output_format.nil?
|
|
361
|
-
prepared_cluster_data.map { |k, v| "- #{k.to_s.upcase}: #{v}" }.join("\n").strip
|
|
362
|
-
else
|
|
363
|
-
prepared_cluster_data
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
Uffizzi.ui.say(rendered_cluster_data)
|
|
367
|
-
end
|
|
368
|
-
|
|
369
350
|
def handle_succeed_create_response(cluster_data)
|
|
370
351
|
kubeconfig_path = options[:kubeconfig] || KubeconfigService.default_path
|
|
371
352
|
is_update_current_context = options[:'update-current-context']
|
|
@@ -432,20 +413,6 @@ module Uffizzi
|
|
|
432
413
|
Psych.safe_load(Base64.decode64(kubeconfig))
|
|
433
414
|
end
|
|
434
415
|
|
|
435
|
-
def fetch_cluster_data(project_slug, cluster_name)
|
|
436
|
-
params = {
|
|
437
|
-
cluster_name: cluster_name,
|
|
438
|
-
oidc_token: ConfigFile.read_option(:oidc_token),
|
|
439
|
-
}
|
|
440
|
-
response = get_cluster(ConfigFile.read_option(:server), project_slug, params)
|
|
441
|
-
|
|
442
|
-
if ResponseHelper.ok?(response)
|
|
443
|
-
response.dig(:body, :cluster)
|
|
444
|
-
else
|
|
445
|
-
ResponseHelper.handle_failed_response(response)
|
|
446
|
-
end
|
|
447
|
-
end
|
|
448
|
-
|
|
449
416
|
def save_previous_current_context(kubeconfig_path, current_context)
|
|
450
417
|
return if kubeconfig_path.nil? || ConfigHelper.previous_current_context_by_path(kubeconfig_path).present?
|
|
451
418
|
|
|
@@ -458,5 +425,25 @@ module Uffizzi
|
|
|
458
425
|
Uffizzi.ui.say('Please update the current context or provide a cluster name.')
|
|
459
426
|
Uffizzi.ui.say('$uffizzi cluster sleep my-cluster')
|
|
460
427
|
end
|
|
428
|
+
|
|
429
|
+
def cluster_api_connection_params
|
|
430
|
+
{
|
|
431
|
+
server: server,
|
|
432
|
+
project_slug: project_slug,
|
|
433
|
+
oidc_token: oidc_token,
|
|
434
|
+
}
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def oidc_token
|
|
438
|
+
@oidc_token ||= ConfigFile.read_option(:oidc_token)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def project_slug
|
|
442
|
+
@project_slug ||= options[:project].nil? ? ConfigFile.read_option(:project) : options[:project]
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def server
|
|
446
|
+
@server ||= ConfigFile.read_option(:server)
|
|
447
|
+
end
|
|
461
448
|
end
|
|
462
449
|
end
|
data/lib/uffizzi/cli/dev.rb
CHANGED
|
@@ -15,67 +15,110 @@ module Uffizzi
|
|
|
15
15
|
method_option :kubeconfig, type: :string
|
|
16
16
|
method_option :'k8s-version', required: false, type: :string
|
|
17
17
|
def start(config_path = 'skaffold.yaml')
|
|
18
|
-
|
|
18
|
+
run('start', config_path: config_path)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc 'stop', 'Stop dev environment'
|
|
22
|
+
def stop
|
|
23
|
+
run('stop')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc 'describe', 'Describe dev environment'
|
|
27
|
+
def describe
|
|
28
|
+
run('describe')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc 'delete', 'Delete dev environment'
|
|
32
|
+
def delete
|
|
33
|
+
run('delete')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def run(command, command_args = {})
|
|
39
|
+
Uffizzi::AuthHelper.check_login
|
|
40
|
+
|
|
41
|
+
case command
|
|
42
|
+
when 'start'
|
|
43
|
+
handle_start_command(command_args)
|
|
44
|
+
when 'stop'
|
|
45
|
+
handle_stop_command
|
|
46
|
+
when 'describe'
|
|
47
|
+
handle_describe_command
|
|
48
|
+
when 'delete'
|
|
49
|
+
handle_delete_command
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_start_command(command_args)
|
|
54
|
+
config_path = command_args[:config_path]
|
|
19
55
|
DevService.check_skaffold_existence
|
|
20
|
-
DevService.
|
|
56
|
+
DevService.check_no_running_process!
|
|
21
57
|
DevService.check_skaffold_config_existence(config_path)
|
|
22
|
-
|
|
23
|
-
|
|
58
|
+
|
|
59
|
+
if dev_environment.empty?
|
|
60
|
+
DevService.set_startup_state
|
|
61
|
+
cluster_name = start_create_cluster
|
|
62
|
+
wait_cluster_creation(cluster_name)
|
|
63
|
+
DevService.set_dev_environment_config(cluster_name, config_path, options)
|
|
64
|
+
DevService.set_cluster_deployed_state
|
|
65
|
+
end
|
|
24
66
|
|
|
25
67
|
if options[:quiet]
|
|
26
68
|
launch_demonise_skaffold(config_path)
|
|
27
69
|
else
|
|
28
|
-
|
|
29
|
-
end
|
|
30
|
-
ensure
|
|
31
|
-
if defined?(cluster_name).present? && defined?(cluster_id).present?
|
|
32
|
-
kubeconfig = defined?(kubeconfig).present? ? kubeconfig : nil
|
|
33
|
-
handle_delete_cluster(cluster_id, cluster_name, kubeconfig)
|
|
70
|
+
launch_basic_skaffold(config_path)
|
|
34
71
|
end
|
|
35
72
|
end
|
|
36
73
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
74
|
+
def handle_stop_command
|
|
75
|
+
DevService.check_running_process!
|
|
76
|
+
DevService.stop_process
|
|
77
|
+
Uffizzi.ui.say('Uffizzi dev was stopped')
|
|
78
|
+
end
|
|
40
79
|
|
|
41
|
-
|
|
42
|
-
|
|
80
|
+
def handle_describe_command
|
|
81
|
+
DevService.check_environment_exist!
|
|
43
82
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
83
|
+
cluster_data = fetch_dev_env_cluster!
|
|
84
|
+
cluster_render_data = ClusterService.build_render_data(cluster_data)
|
|
85
|
+
dev_environment_render_data = cluster_render_data.merge(config_path: dev_environment[:config_path])
|
|
86
|
+
|
|
87
|
+
Uffizzi.ui.output_format = Uffizzi::UI::Shell::PRETTY_LIST
|
|
88
|
+
Uffizzi.ui.say(dev_environment_render_data)
|
|
49
89
|
end
|
|
50
90
|
|
|
51
|
-
|
|
91
|
+
def handle_delete_command
|
|
92
|
+
DevService.check_environment_exist!
|
|
93
|
+
|
|
94
|
+
if DevService.process_running?
|
|
95
|
+
DevService.stop_process
|
|
96
|
+
Uffizzi.ui.say('Uffizzi dev was stopped')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
cluster_data = fetch_dev_env_cluster!
|
|
100
|
+
handle_delete_cluster(cluster_data)
|
|
101
|
+
DevService.clear_dev_environment_config
|
|
102
|
+
end
|
|
52
103
|
|
|
53
104
|
def start_create_cluster
|
|
54
|
-
params = cluster_creation_params
|
|
55
|
-
name: ClusterService.generate_name,
|
|
56
|
-
creation_source: ClusterService::MANUAL_CREATION_SOURCE,
|
|
57
|
-
k8s_version: options[:"k8s-version"],
|
|
58
|
-
)
|
|
105
|
+
params = cluster_creation_params
|
|
59
106
|
Uffizzi.ui.say('Start creating a cluster')
|
|
60
|
-
response = create_cluster(
|
|
107
|
+
response = create_cluster(server, project_slug, params)
|
|
61
108
|
return ResponseHelper.handle_failed_response(response) unless ResponseHelper.created?(response)
|
|
62
109
|
|
|
63
|
-
|
|
64
|
-
cluster_name = response.dig(:body, :cluster, :name)
|
|
65
|
-
|
|
66
|
-
[cluster_id, cluster_name]
|
|
110
|
+
response.dig(:body, :cluster, :name)
|
|
67
111
|
end
|
|
68
112
|
|
|
69
113
|
def wait_cluster_creation(cluster_name)
|
|
70
114
|
Uffizzi.ui.say('Checking the cluster status...')
|
|
71
|
-
cluster_data = ClusterService.wait_cluster_deploy(project_slug, cluster_name,
|
|
115
|
+
cluster_data = ClusterService.wait_cluster_deploy(project_slug, cluster_name, oidc_token)
|
|
72
116
|
|
|
73
117
|
if ClusterService.failed?(cluster_data[:state])
|
|
74
118
|
Uffizzi.ui.say_error_and_exit("Cluster with name: #{cluster_name} failed to be created.")
|
|
75
119
|
end
|
|
76
120
|
|
|
77
121
|
handle_succeed_cluster_creation(cluster_data)
|
|
78
|
-
parse_kubeconfig(cluster_data[:kubeconfig])
|
|
79
122
|
end
|
|
80
123
|
|
|
81
124
|
def handle_succeed_cluster_creation(cluster_data)
|
|
@@ -109,30 +152,30 @@ module Uffizzi
|
|
|
109
152
|
ConfigFile.write_option(:clusters, clusters_config)
|
|
110
153
|
end
|
|
111
154
|
|
|
112
|
-
def cluster_creation_params
|
|
113
|
-
oidc_token = Uffizzi::ConfigFile.read_option(:oidc_token)
|
|
114
|
-
|
|
155
|
+
def cluster_creation_params
|
|
115
156
|
{
|
|
116
157
|
cluster: {
|
|
117
|
-
name:
|
|
158
|
+
name: ClusterService.generate_name,
|
|
118
159
|
manifest: nil,
|
|
119
|
-
creation_source:
|
|
120
|
-
k8s_version:
|
|
160
|
+
creation_source: ClusterService::MANUAL_CREATION_SOURCE,
|
|
161
|
+
k8s_version: options[:"k8s-version"],
|
|
121
162
|
},
|
|
122
163
|
token: oidc_token,
|
|
123
164
|
}
|
|
124
165
|
end
|
|
125
166
|
|
|
126
|
-
def handle_delete_cluster(
|
|
127
|
-
|
|
167
|
+
def handle_delete_cluster(cluster_data)
|
|
168
|
+
cluster_id = cluster_data[:id]
|
|
169
|
+
cluster_name = cluster_data[:name]
|
|
170
|
+
kubeconfig = parse_kubeconfig(cluster_data[:kubeconfig])
|
|
128
171
|
|
|
129
172
|
exclude_kubeconfig(cluster_id, kubeconfig) if kubeconfig.present?
|
|
130
173
|
|
|
131
174
|
params = {
|
|
132
175
|
cluster_name: cluster_name,
|
|
133
|
-
oidc_token:
|
|
176
|
+
oidc_token: oidc_token,
|
|
134
177
|
}
|
|
135
|
-
response = delete_cluster(
|
|
178
|
+
response = delete_cluster(server, project_slug, params)
|
|
136
179
|
|
|
137
180
|
if ResponseHelper.no_content?(response)
|
|
138
181
|
Uffizzi.ui.say("Cluster #{cluster_name} deleted")
|
|
@@ -179,20 +222,59 @@ module Uffizzi
|
|
|
179
222
|
def launch_demonise_skaffold(config_path)
|
|
180
223
|
Uffizzi.process.daemon(true)
|
|
181
224
|
|
|
182
|
-
at_exit do
|
|
183
|
-
|
|
225
|
+
Uffizzi.at_exit do
|
|
226
|
+
DevService.stop_process
|
|
184
227
|
end
|
|
185
228
|
|
|
229
|
+
DevService.save_pid
|
|
186
230
|
File.delete(DevService.logs_path) if File.exist?(DevService.logs_path)
|
|
187
|
-
File.write(DevService.pid_path, Uffizzi.process.pid)
|
|
188
231
|
DevService.start_check_pid_file_existence
|
|
189
232
|
DevService.start_demonised_skaffold(config_path, options)
|
|
190
233
|
rescue StandardError => e
|
|
191
234
|
File.open(DevService.logs_path, 'a') { |f| f.puts(e.message) }
|
|
192
235
|
end
|
|
193
236
|
|
|
237
|
+
def launch_basic_skaffold(config_path)
|
|
238
|
+
Uffizzi.at_exit do
|
|
239
|
+
DevService.stop_process
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
DevService.save_pid
|
|
243
|
+
DevService.start_check_pid_file_existence
|
|
244
|
+
DevService.start_basic_skaffold(config_path, options)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def fetch_dev_env_cluster!
|
|
248
|
+
if DevService.startup?
|
|
249
|
+
Uffizzi.ui.say_error_and_exit('Dev environment not started yet')
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
cluster_name = dev_environment[:cluster_name]
|
|
253
|
+
ClusterService.fetch_cluster_data(cluster_name, **cluster_api_connection_params)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def dev_environment
|
|
257
|
+
@dev_environment ||= DevService.dev_environment
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def cluster_api_connection_params
|
|
261
|
+
{
|
|
262
|
+
server: server,
|
|
263
|
+
project_slug: project_slug,
|
|
264
|
+
oidc_token: oidc_token,
|
|
265
|
+
}
|
|
266
|
+
end
|
|
267
|
+
|
|
194
268
|
def project_slug
|
|
195
269
|
@project_slug ||= ConfigFile.read_option(:project)
|
|
196
270
|
end
|
|
271
|
+
|
|
272
|
+
def oidc_token
|
|
273
|
+
@oidc_token ||= ConfigFile.read_option(:oidc_token)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def server
|
|
277
|
+
@server ||= ConfigFile.read_option(:server)
|
|
278
|
+
end
|
|
197
279
|
end
|
|
198
280
|
end
|
|
@@ -52,6 +52,14 @@ module Uffizzi
|
|
|
52
52
|
cluster_previous_current_contexts.detect { |c| c[:kubeconfig_path] == path }
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
def set_dev_environment(cluster_name, params = {})
|
|
56
|
+
{ cluster_name: cluster_name }.merge(params)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dev_environment
|
|
60
|
+
read_option_from_config(:dev_environment) || {}
|
|
61
|
+
end
|
|
62
|
+
|
|
55
63
|
private
|
|
56
64
|
|
|
57
65
|
def clusters
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'uffizzi/response_helper'
|
|
3
4
|
require 'uffizzi/clients/api/api_client'
|
|
4
5
|
|
|
5
6
|
class ClusterService
|
|
@@ -104,5 +105,28 @@ class ClusterService
|
|
|
104
105
|
regex = /\A[a-zA-Z0-9-]*\z/
|
|
105
106
|
regex.match?(name)
|
|
106
107
|
end
|
|
108
|
+
|
|
109
|
+
def fetch_cluster_data(cluster_name, server:, project_slug:, oidc_token:)
|
|
110
|
+
params = {
|
|
111
|
+
cluster_name: cluster_name,
|
|
112
|
+
oidc_token: oidc_token,
|
|
113
|
+
}
|
|
114
|
+
response = get_cluster(server, project_slug, params)
|
|
115
|
+
|
|
116
|
+
if Uffizzi::ResponseHelper.ok?(response)
|
|
117
|
+
response.dig(:body, :cluster)
|
|
118
|
+
else
|
|
119
|
+
Uffizzi::ResponseHelper.handle_failed_response(response)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_render_data(cluster_data)
|
|
124
|
+
{
|
|
125
|
+
name: cluster_data[:name],
|
|
126
|
+
status: cluster_data[:state],
|
|
127
|
+
created: Time.strptime(cluster_data[:created_at], '%Y-%m-%dT%H:%M:%S.%N').strftime('%a %b %d %H:%M:%S %Y'),
|
|
128
|
+
url: cluster_data[:host],
|
|
129
|
+
}
|
|
130
|
+
end
|
|
107
131
|
end
|
|
108
132
|
end
|
|
@@ -3,27 +3,70 @@
|
|
|
3
3
|
require 'uffizzi/clients/api/api_client'
|
|
4
4
|
|
|
5
5
|
class DevService
|
|
6
|
+
DEFAULT_REGISTRY_REPO = 'registry.uffizzi.com'
|
|
7
|
+
STARTUP_STATE = 'startup'
|
|
8
|
+
CLUSTER_DEPLOYED_STATE = 'cluster_deployed'
|
|
9
|
+
|
|
6
10
|
class << self
|
|
7
11
|
include ApiClient
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
def check_no_running_process!
|
|
14
|
+
if process_running?
|
|
15
|
+
Uffizzi.ui.say_error_and_exit("You have already started uffizzi dev. To stop the process do 'uffizzi dev stop'")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
10
18
|
|
|
11
|
-
def
|
|
12
|
-
|
|
19
|
+
def check_running_process!
|
|
20
|
+
unless process_running?
|
|
21
|
+
Uffizzi.ui.say_error_and_exit('Uffizzi dev is not running')
|
|
22
|
+
end
|
|
23
|
+
end
|
|
13
24
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
def check_environment_exist!
|
|
26
|
+
if dev_environment.empty?
|
|
27
|
+
Uffizzi.ui.say_error_and_exit('Uffizzi dev does not exist')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
17
30
|
|
|
18
|
-
|
|
31
|
+
def stop_process
|
|
32
|
+
dev_pid = running_pid
|
|
33
|
+
skaffold_pid = running_skaffold_pid
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
Uffizzi.process.kill('INT', skaffold_pid)
|
|
37
|
+
rescue Errno::ESRCH
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
wait_process_stop(skaffold_pid)
|
|
41
|
+
delete_pid
|
|
42
|
+
|
|
43
|
+
Uffizzi.process.kill('INT', dev_pid)
|
|
19
44
|
rescue Errno::ESRCH
|
|
20
|
-
|
|
45
|
+
delete_pid
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def wait_process_stop(pid)
|
|
49
|
+
loop do
|
|
50
|
+
Uffizzi.process.kill(0, pid)
|
|
51
|
+
sleep(1)
|
|
52
|
+
end
|
|
53
|
+
rescue Errno::ESRCH
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def process_running?
|
|
57
|
+
pid = running_pid
|
|
58
|
+
return false unless pid.positive?
|
|
59
|
+
|
|
60
|
+
Uffizzi.process.kill(0, pid.to_i)
|
|
61
|
+
true
|
|
62
|
+
rescue Errno::ESRCH
|
|
63
|
+
false
|
|
21
64
|
end
|
|
22
65
|
|
|
23
66
|
def start_check_pid_file_existence
|
|
24
|
-
|
|
67
|
+
Uffizzi.thread.new do
|
|
25
68
|
loop do
|
|
26
|
-
|
|
69
|
+
stop_process unless File.exist?(pid_path)
|
|
27
70
|
sleep(1)
|
|
28
71
|
end
|
|
29
72
|
end
|
|
@@ -33,7 +76,12 @@ class DevService
|
|
|
33
76
|
Uffizzi.ui.say('Start skaffold')
|
|
34
77
|
cmd = build_skaffold_dev_command(config_path, options)
|
|
35
78
|
|
|
79
|
+
Uffizzi.signal.trap('INT') {}
|
|
80
|
+
|
|
36
81
|
Uffizzi.ui.popen2e(cmd) do |_stdin, stdout_and_stderr, wait_thr|
|
|
82
|
+
pid = wait_thr.pid
|
|
83
|
+
skaffold_pid = find_skaffold_pid(pid)
|
|
84
|
+
save_skaffold_pid(skaffold_pid)
|
|
37
85
|
stdout_and_stderr.each { |l| Uffizzi.ui.say(l) }
|
|
38
86
|
wait_thr.value
|
|
39
87
|
end
|
|
@@ -44,6 +92,10 @@ class DevService
|
|
|
44
92
|
cmd = build_skaffold_dev_command(config_path, options)
|
|
45
93
|
|
|
46
94
|
Uffizzi.ui.popen2e(cmd) do |_stdin, stdout_and_stderr, wait_thr|
|
|
95
|
+
pid = wait_thr.pid
|
|
96
|
+
skaffold_pid = find_skaffold_pid(pid)
|
|
97
|
+
save_skaffold_pid(skaffold_pid)
|
|
98
|
+
|
|
47
99
|
File.open(logs_path, 'a') do |f|
|
|
48
100
|
stdout_and_stderr.each do |line|
|
|
49
101
|
f.puts(line)
|
|
@@ -80,6 +132,10 @@ class DevService
|
|
|
80
132
|
File.join(Uffizzi::ConfigFile::CONFIG_DIR, 'uffizzi_dev.pid')
|
|
81
133
|
end
|
|
82
134
|
|
|
135
|
+
def skaffold_pid_path
|
|
136
|
+
File.join(Uffizzi::ConfigFile::CONFIG_DIR, 'skaffold_dev.pid')
|
|
137
|
+
end
|
|
138
|
+
|
|
83
139
|
def logs_path
|
|
84
140
|
File.join(Uffizzi::ConfigFile::CONFIG_DIR, 'uffizzi_dev.log')
|
|
85
141
|
end
|
|
@@ -104,5 +160,83 @@ class DevService
|
|
|
104
160
|
|
|
105
161
|
File.expand_path(path)
|
|
106
162
|
end
|
|
163
|
+
|
|
164
|
+
def running_pid
|
|
165
|
+
return nil.to_i unless File.exist?(pid_path)
|
|
166
|
+
|
|
167
|
+
File.read(pid_path).to_i
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def save_pid
|
|
171
|
+
File.write(pid_path, Uffizzi.process.pid)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def delete_pid
|
|
175
|
+
File.delete(pid_path) if File.exist?(pid_path)
|
|
176
|
+
File.delete(skaffold_pid_path) if File.exist?(skaffold_pid_path)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def running_skaffold_pid
|
|
180
|
+
return nil.to_i unless File.exist?(skaffold_pid_path)
|
|
181
|
+
|
|
182
|
+
File.read(skaffold_pid_path).to_i
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def save_skaffold_pid(pid)
|
|
186
|
+
File.write(skaffold_pid_path, pid)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def set_dev_environment_config(cluster_name, config_path, options)
|
|
190
|
+
params = options.merge(config_path: File.expand_path(config_path))
|
|
191
|
+
new_dev_environment = Uffizzi::ConfigHelper.set_dev_environment(cluster_name, params)
|
|
192
|
+
Uffizzi::ConfigFile.write_option(:dev_environment, new_dev_environment)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def set_startup_state
|
|
196
|
+
new_dev_environment = dev_environment.merge(state: STARTUP_STATE)
|
|
197
|
+
Uffizzi::ConfigFile.write_option(:dev_environment, new_dev_environment)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def set_cluster_deployed_state
|
|
201
|
+
new_dev_environment = dev_environment.merge(state: CLUSTER_DEPLOYED_STATE)
|
|
202
|
+
Uffizzi::ConfigFile.write_option(:dev_environment, new_dev_environment)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def startup?
|
|
206
|
+
dev_environment[:state] == STARTUP_STATE
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def clear_dev_environment_config
|
|
210
|
+
Uffizzi::ConfigFile.write_option(:dev_environment, {})
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def dev_environment
|
|
214
|
+
Uffizzi::ConfigHelper.dev_environment
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def find_skaffold_pid(pid)
|
|
218
|
+
pid_regex = /\w*#{pid}.*skaffold dev/
|
|
219
|
+
io = Uffizzi.ui.popen('ps -ef')
|
|
220
|
+
processes = io.readlines.select { |l| l.match?(pid_regex) }
|
|
221
|
+
|
|
222
|
+
if processes.count.zero?
|
|
223
|
+
raise StandardError.new('Can\'t find skaffold process pid')
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# HACK: For MacOS
|
|
227
|
+
if processes.count == 1
|
|
228
|
+
current_pid = processes[0].gsub(/\s+/, ' ').lstrip.split[1]
|
|
229
|
+
return pid if current_pid.to_s == pid.to_s
|
|
230
|
+
|
|
231
|
+
raise StandardError.new('Can\'t find skaffold process pid')
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# HACK: For Linux
|
|
235
|
+
parent_process = processes
|
|
236
|
+
.map { |ps| ps.gsub(/\s+/, ' ').lstrip.split }
|
|
237
|
+
.detect { |ps| ps[2].to_s == pid.to_s }
|
|
238
|
+
|
|
239
|
+
parent_process[1]
|
|
240
|
+
end
|
|
107
241
|
end
|
|
108
242
|
end
|
data/lib/uffizzi/shell.rb
CHANGED
|
@@ -11,6 +11,7 @@ module Uffizzi
|
|
|
11
11
|
|
|
12
12
|
PRETTY_JSON = 'pretty-json'
|
|
13
13
|
REGULAR_JSON = 'json'
|
|
14
|
+
PRETTY_LIST = 'pretty-list'
|
|
14
15
|
|
|
15
16
|
def initialize
|
|
16
17
|
@shell = Thor::Shell::Basic.new
|
|
@@ -55,6 +56,10 @@ module Uffizzi
|
|
|
55
56
|
$stdout.stat.pipe?
|
|
56
57
|
end
|
|
57
58
|
|
|
59
|
+
def popen(command)
|
|
60
|
+
IO.popen(command)
|
|
61
|
+
end
|
|
62
|
+
|
|
58
63
|
def popen2e(command, &block)
|
|
59
64
|
Open3.popen2e(command, &block)
|
|
60
65
|
end
|
|
@@ -73,12 +78,25 @@ module Uffizzi
|
|
|
73
78
|
JSON.pretty_generate(data)
|
|
74
79
|
end
|
|
75
80
|
|
|
81
|
+
def format_to_pretty_list(data)
|
|
82
|
+
case data
|
|
83
|
+
when Array
|
|
84
|
+
data.map { |v| format_to_pretty_list(v) }.join("\n\n")
|
|
85
|
+
when Hash
|
|
86
|
+
data.map { |k, v| "- #{k.to_s.upcase}: #{v}" }.join("\n").strip
|
|
87
|
+
else
|
|
88
|
+
data
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
76
92
|
def format_message(message)
|
|
77
93
|
case output_format
|
|
78
94
|
when PRETTY_JSON
|
|
79
95
|
format_to_pretty_json(message)
|
|
80
96
|
when REGULAR_JSON
|
|
81
97
|
format_to_json(message)
|
|
98
|
+
when PRETTY_LIST
|
|
99
|
+
format_to_pretty_list(message)
|
|
82
100
|
else
|
|
83
101
|
message
|
|
84
102
|
end
|
data/lib/uffizzi/version.rb
CHANGED
data/lib/uffizzi.rb
CHANGED
data/man/uffizzi
CHANGED
|
@@ -33,6 +33,9 @@ GROUP is one of the following:
|
|
|
33
33
|
project
|
|
34
34
|
Manage Uffizzi project resources including compose files for
|
|
35
35
|
specifying compose environment (preview) configurations and secrets
|
|
36
|
+
|
|
37
|
+
dev
|
|
38
|
+
Creates a Uffizzi cluster preconfigured for development workflows
|
|
36
39
|
.fi
|
|
37
40
|
.SH "COMMAND"
|
|
38
41
|
.nf
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
.\" generated with Ronn-NG/v0.9.1
|
|
2
|
+
.\" http://github.com/apjanke/ronn-ng/tree/0.9.1
|
|
3
|
+
.TH "UFFIZZI\-DEV\-DESCRIBE" "" "October 2023" ""
|
|
4
|
+
.SH "NAME"
|
|
5
|
+
\fBuffizzi\-dev\-describe\fR
|
|
6
|
+
.P
|
|
7
|
+
$ uffizzi dev describe \-h uffizzi\-dev\-describe \- show metadata for a dev environment ================================================================
|
|
8
|
+
.SH "SYNOPSIS"
|
|
9
|
+
.nf
|
|
10
|
+
uffizzi dev describe <NAME>
|
|
11
|
+
.fi
|
|
12
|
+
.SH "DESCRIPTION"
|
|
13
|
+
.nf
|
|
14
|
+
Shows metadata for a dev environment
|
|
15
|
+
|
|
16
|
+
This command can fail for the following reasons:
|
|
17
|
+
\- The dev environment specified does not exist\.
|
|
18
|
+
\- The dev environment specified belongs to a different project\.
|
|
19
|
+
|
|
20
|
+
For more information on Uffizzi clusters, see:
|
|
21
|
+
https://docs\.uffizzi\.com/references/cli/
|
|
22
|
+
.fi
|
|
23
|
+
.SH "POSITIONAL ARGUMENTS"
|
|
24
|
+
.nf
|
|
25
|
+
[NAME]
|
|
26
|
+
NAME for the dev environment you want to describe\.
|
|
27
|
+
This is an optional argument\.
|
|
28
|
+
.fi
|
|
29
|
+
.SH "EXAMPLES"
|
|
30
|
+
.nf
|
|
31
|
+
The following command prints metadata for the dev
|
|
32
|
+
environment:
|
|
33
|
+
|
|
34
|
+
$ uffizzi dev describe
|
|
35
|
+
.fi
|
|
36
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
$ uffizzi dev describe -h
|
|
2
|
+
uffizzi-dev-describe - show metadata for a dev environment
|
|
3
|
+
================================================================
|
|
4
|
+
|
|
5
|
+
## SYNOPSIS
|
|
6
|
+
uffizzi dev describe <NAME>
|
|
7
|
+
|
|
8
|
+
## DESCRIPTION
|
|
9
|
+
Shows metadata for a dev environment
|
|
10
|
+
|
|
11
|
+
This command can fail for the following reasons:
|
|
12
|
+
- The dev environment specified does not exist.
|
|
13
|
+
- The dev environment specified belongs to a different project.
|
|
14
|
+
|
|
15
|
+
For more information on Uffizzi clusters, see:
|
|
16
|
+
https://docs.uffizzi.com/references/cli/
|
|
17
|
+
|
|
18
|
+
## POSITIONAL ARGUMENTS
|
|
19
|
+
[NAME]
|
|
20
|
+
NAME for the dev environment you want to describe.
|
|
21
|
+
This is an optional argument.
|
|
22
|
+
|
|
23
|
+
## EXAMPLES
|
|
24
|
+
The following command prints metadata for the dev
|
|
25
|
+
environment:
|
|
26
|
+
|
|
27
|
+
$ uffizzi dev describe
|
data/man/uffizzi.ronn
CHANGED
|
@@ -29,6 +29,9 @@ uffizzi - manage Uffizzi resources
|
|
|
29
29
|
Manage Uffizzi project resources including compose files for
|
|
30
30
|
specifying compose environment (preview) configurations and secrets
|
|
31
31
|
|
|
32
|
+
dev
|
|
33
|
+
Creates a Uffizzi cluster preconfigured for development workflows
|
|
34
|
+
|
|
32
35
|
## COMMAND
|
|
33
36
|
COMMAND is one of the following:
|
|
34
37
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: uffizzi-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.2.
|
|
4
|
+
version: 2.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Josh Thurman
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2023-10-
|
|
12
|
+
date: 2023-10-25 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activesupport
|
|
@@ -522,6 +522,8 @@ files:
|
|
|
522
522
|
- man/uffizzi-connect-ghcr.ronn
|
|
523
523
|
- man/uffizzi-connect.ronn
|
|
524
524
|
- man/uffizzi-dev
|
|
525
|
+
- man/uffizzi-dev-describe
|
|
526
|
+
- man/uffizzi-dev-describe.ronn
|
|
525
527
|
- man/uffizzi-dev-start
|
|
526
528
|
- man/uffizzi-dev-start.ronn
|
|
527
529
|
- man/uffizzi-dev-stop
|