Skip to content

Commit 2dbf08c

Browse files
committed
Add services urls
1 parent 1344d63 commit 2dbf08c

7 files changed

Lines changed: 277 additions & 20 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ PROJECTS
7979
BUILDS
8080
deploio builds List all builds
8181
deploio builds -a APP List builds for a specific app
82+
83+
SERVICES
84+
deploio services List all services
85+
deploio services -p PROJECT List services in a specific project
86+
deploio services -p PROJECT --url List services with connection URLs (requires -p)
87+
8288
LOGS
8389
deploio logs -a APP Show recent logs
8490
deploio logs -a APP --tail Stream logs continuously

lib/deploio/cli.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require_relative "commands/builds"
77
require_relative "commands/orgs"
88
require_relative "commands/projects"
9+
require_relative "commands/services"
910
require_relative "completion_generator"
1011

1112
module Deploio
@@ -45,6 +46,9 @@ def completion
4546
desc "projects COMMAND", "Project management commands"
4647
subcommand "projects", Commands::Projects
4748

49+
desc "services COMMAND", "Service management commands"
50+
subcommand "services", Commands::Services
51+
4852
desc "builds COMMAND", "Build management commands"
4953
subcommand "builds", Commands::Builds
5054

lib/deploio/commands/services.rb

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# frozen_string_literal: true
2+
3+
module Deploio
4+
module Commands
5+
class Services < Thor
6+
include SharedOptions
7+
8+
namespace "services"
9+
10+
class_option :json, type: :boolean, default: false, desc: "Output as JSON"
11+
12+
default_task :list
13+
14+
desc "list", "List services (all or filtered by project)"
15+
method_option :project, aliases: "-p", type: :string,
16+
desc: "Filter by project name"
17+
method_option :url, aliases: "-u", type: :boolean, default: false,
18+
desc: "Show connection URL for each service (requires --project)"
19+
def list
20+
setup_options
21+
22+
if merged_options[:url] && !merged_options[:project]
23+
Output.error("The --url option requires --project to be specified")
24+
Output.info("Fetching URLs for all services is too slow. Please filter by project first.")
25+
Output.info("Example: deploio services -p myproject --url")
26+
exit 1
27+
end
28+
29+
project = merged_options[:project] ? resolve_project(merged_options[:project]) : nil
30+
all_services = @nctl.get_all_services(project: project)
31+
32+
if options[:json]
33+
puts JSON.pretty_generate(all_services)
34+
return
35+
end
36+
37+
if all_services.empty?
38+
msg = project ? "No services found in project #{merged_options[:project]}" : "No services found"
39+
Output.warning(msg) unless merged_options[:dry_run]
40+
return
41+
end
42+
43+
show_url = merged_options[:url]
44+
current_org = @nctl.current_org
45+
rows = all_services.map do |service|
46+
metadata = service["metadata"] || {}
47+
status = service["status"] || {}
48+
conditions = status["conditions"] || []
49+
ready_condition = conditions.find { |c| c["type"] == "Ready" }
50+
51+
namespace = metadata["namespace"] || ""
52+
name = metadata["name"] || ""
53+
type = service["_type"] || "-"
54+
55+
row = [
56+
short_service_name(namespace, name, current_org),
57+
project_from_namespace(namespace, current_org),
58+
type,
59+
presence(ready_condition&.dig("status"))
60+
]
61+
62+
if show_url
63+
url = @nctl.get_service_connection_string(type, name, project: namespace)
64+
row << presence(url)
65+
end
66+
67+
row
68+
end
69+
70+
headers = %w[SERVICE PROJECT TYPE READY]
71+
headers << "URL" if show_url
72+
Output.table(rows, headers: headers)
73+
end
74+
75+
private
76+
77+
def resolve_project(project)
78+
current_org = @nctl.current_org
79+
# Don't prepend org if:
80+
# - No org context
81+
# - Project already contains a hyphen (already qualified)
82+
# - Project equals the org name (special case for default project)
83+
if current_org && !project.include?("-") && project != current_org
84+
"#{current_org}-#{project}"
85+
else
86+
project
87+
end
88+
end
89+
90+
def presence(value, default: "-")
91+
return default if value.nil? || value.to_s.empty?
92+
93+
value.to_s
94+
end
95+
96+
def short_service_name(namespace, name, current_org)
97+
project = project_from_namespace(namespace, current_org)
98+
"#{project}-#{name}"
99+
end
100+
101+
def project_from_namespace(namespace, current_org)
102+
if current_org && namespace.start_with?("#{current_org}-")
103+
namespace.delete_prefix("#{current_org}-")
104+
else
105+
namespace
106+
end
107+
end
108+
end
109+
end
110+
end

lib/deploio/completion_generator.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def default_option_completers
5252
{
5353
"app" => "app:_#{program_name}_apps_list",
5454
"size" => "size:(micro mini standard)"
55+
"project" => "project:_#{program_name}_projects_list",
5556
}
5657
end
5758

lib/deploio/nctl_client.rb

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,6 @@ def get_app(app_ref)
5555
{}
5656
end
5757

58-
def get_app_stats(app_ref)
59-
capture("get", "app", app_ref.app_name,
60-
"--project", app_ref.project_name,
61-
"-o", "stats")
62-
end
63-
6458
def get_all_builds
6559
output = capture("get", "builds", "-A", "-o", "json")
6660
return [] if output.nil? || output.empty?
@@ -89,26 +83,61 @@ def get_builds(app_ref)
8983
[]
9084
end
9185

92-
def edit_app(app_ref)
93-
exec_passthrough("edit", "app", app_ref.app_name,
94-
"--project", app_ref.project_name)
86+
def get_all_services(project: nil)
87+
service_types = %w[keyvaluestore postgres mysql opensearch]
88+
all_services = []
89+
90+
service_types.each do |type|
91+
services = get_services_by_type(type, project: project)
92+
services.each { |s| s["_type"] = type }
93+
all_services.concat(services)
94+
end
95+
96+
all_services
9597
end
9698

97-
def create_app(project, app_name, git_url:, git_revision:, size: "mini")
98-
run("create", "app", app_name,
99-
"--project", project,
100-
"--git-url", git_url,
101-
"--git-revision", git_revision,
102-
"--size", size)
99+
def get_services_by_type(type, project: nil)
100+
args = ["get", type]
101+
if project
102+
args += ["--project", project]
103+
else
104+
args << "-A"
105+
end
106+
args += ["-o", "json"]
107+
108+
output = capture(*args)
109+
return [] if output.nil? || output.empty?
110+
111+
data = JSON.parse(output)
112+
data.is_a?(Array) ? data : (data["items"] || [])
113+
rescue JSON::ParserError
114+
[]
115+
rescue Deploio::NctlError
116+
[]
103117
end
104118

105-
def delete_app(app_ref)
106-
run("delete", "app", app_ref.app_name,
107-
"--project", app_ref.project_name)
119+
def get_service(type, name, project:)
120+
output = capture("get", type, name, "--project", project, "-o", "json")
121+
return nil if output.nil? || output.empty?
122+
123+
JSON.parse(output)
124+
rescue JSON::ParserError
125+
nil
126+
rescue Deploio::NctlError
127+
nil
108128
end
109129

110-
def create_project(project_name)
111-
run("create", "project", project_name)
130+
def get_service_connection_string(type, name, project:)
131+
case type
132+
when "postgres", "mysql"
133+
capture("get", type, name, "--project", project, "--print-connection-string").strip
134+
when "keyvaluestore"
135+
build_keyvaluestore_connection_string(name, project)
136+
when "opensearch"
137+
build_opensearch_connection_string(name, project)
138+
end
139+
rescue Deploio::NctlError
140+
nil
112141
end
113142

114143
def get_projects
@@ -269,5 +298,31 @@ def check_nctl_version
269298
raise Deploio::NctlError,
270299
"nctl version #{version} is too old. Need #{REQUIRED_VERSION}+. Run: brew upgrade nctl"
271300
end
301+
302+
def build_keyvaluestore_connection_string(name, project)
303+
data = get_service("keyvaluestore", name, project: project)
304+
return nil unless data
305+
306+
at_provider = data.dig("status", "atProvider") || {}
307+
fqdn = at_provider["fqdn"]
308+
return nil if fqdn.nil? || fqdn.empty?
309+
310+
token = capture("get", "keyvaluestore", name, "--project", project, "--print-token").strip
311+
"rediss://:#{token}@#{fqdn}:6379"
312+
end
313+
314+
def build_opensearch_connection_string(name, project)
315+
data = get_service("opensearch", name, project: project)
316+
return nil unless data
317+
318+
at_provider = data.dig("status", "atProvider") || {}
319+
hosts = at_provider["hosts"] || []
320+
return nil if hosts.empty?
321+
322+
user = capture("get", "opensearch", name, "--project", project, "--print-user").strip
323+
password = capture("get", "opensearch", name, "--project", project, "--print-password").strip
324+
host = hosts.first
325+
"https://#{user}:#{password}@#{host}"
326+
end
272327
end
273328
end

lib/deploio/templates/completion.zsh.erb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,32 @@ _<%= program_name %>_orgs_list() {
2323
fi
2424
}
2525

26+
# Dynamic completion for services
27+
_<%= program_name %>_services_list() {
28+
local -a services
29+
services=(${(f)"$(<%= program_name %> services --json 2>/dev/null | ruby -rjson -e '
30+
data = JSON.parse(STDIN.read) rescue []
31+
orgs_json = `<%= program_name %> orgs --json 2>/dev/null` rescue "[]"
32+
orgs = JSON.parse(orgs_json) rescue []
33+
current_org = orgs.find { |o| o["current"] }&.fetch("name", nil)
34+
35+
data.each do |s|
36+
ns = s.dig("metadata", "namespace") || ""
37+
name = s.dig("metadata", "name") || ""
38+
type = s["_type"] || "unknown"
39+
project = current_org && ns.start_with?("#{current_org}-") ? ns.delete_prefix("#{current_org}-") : ns
40+
short_name = "#{project}-#{name}"
41+
puts "#{short_name}:#{name} (#{type})"
42+
end
43+
' 2>/dev/null)"})
44+
45+
if [[ ${#services[@]} -gt 0 ]]; then
46+
_describe -t services 'available services' services
47+
else
48+
_message 'service (format: project-servicename)'
49+
fi
50+
}
51+
2652
# Dynamic completion for projects
2753
_<%= program_name %>_projects_list() {
2854
local -a projects

test/deploio/cli_services_test.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
require "stringio"
5+
6+
class CLIServicesTest < Minitest::Test
7+
def test_services_list_queries_all_service_types
8+
out, = capture_io do
9+
Deploio::Commands::Services.start(["list", "--dry-run"])
10+
end
11+
12+
# Should query all service types
13+
assert_match(/nctl get keyvaluestore -A -o json/, out)
14+
assert_match(/nctl get postgres -A -o json/, out)
15+
assert_match(/nctl get mysql -A -o json/, out)
16+
assert_match(/nctl get opensearch -A -o json/, out)
17+
end
18+
19+
def test_services_default_task_is_list
20+
out, = capture_io do
21+
Deploio::Commands::Services.start(["--dry-run"])
22+
end
23+
24+
# Default task should query all service types
25+
assert_match(/nctl get keyvaluestore -A -o json/, out)
26+
end
27+
28+
def test_services_via_main_cli_default_task
29+
out, = capture_io do
30+
Deploio::CLI.start(["services", "--dry-run"])
31+
end
32+
33+
assert_match(/nctl get keyvaluestore -A -o json/, out)
34+
end
35+
36+
def test_services_list_with_project_filter
37+
out, = capture_io do
38+
Deploio::Commands::Services.start(["list", "--project", "myproject", "--dry-run"])
39+
end
40+
41+
# Should query with project filter instead of -A
42+
assert_match(/nctl get keyvaluestore --project.*myproject -o json/, out)
43+
assert_match(/nctl get postgres --project.*myproject -o json/, out)
44+
assert_match(/nctl get mysql --project.*myproject -o json/, out)
45+
assert_match(/nctl get opensearch --project.*myproject -o json/, out)
46+
end
47+
48+
def test_services_list_with_project_filter_via_main_cli
49+
out, = capture_io do
50+
Deploio::CLI.start(["services", "-p", "myproject", "--dry-run"])
51+
end
52+
53+
assert_match(/nctl get keyvaluestore --project.*myproject -o json/, out)
54+
end
55+
end

0 commit comments

Comments
 (0)