deploio-cli 0.1.0 → 0.2.0
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/README.md +64 -0
- data/deploio-cli.gemspec +7 -0
- data/lib/deploio/cli.rb +17 -0
- data/lib/deploio/commands/apps.rb +29 -7
- data/lib/deploio/commands/builds.rb +139 -0
- data/lib/deploio/commands/postgresql.rb +131 -0
- data/lib/deploio/commands/postgresql_backups.rb +75 -0
- data/lib/deploio/commands/projects.rb +185 -0
- data/lib/deploio/commands/services.rb +186 -0
- data/lib/deploio/completion_generator.rb +21 -3
- data/lib/deploio/nctl_client.rb +197 -18
- data/lib/deploio/output.rb +40 -0
- data/lib/deploio/pg_database_ref.rb +66 -0
- data/lib/deploio/pg_database_resolver.rb +55 -0
- data/lib/deploio/price_fetcher.rb +201 -0
- data/lib/deploio/shared_options.rb +16 -0
- data/lib/deploio/templates/completion.zsh.erb +134 -1
- data/lib/deploio/version.rb +1 -1
- data/lib/deploio.rb +4 -0
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eae3786b2b0c98a4d02b1ac9037e9536da6cfc90ab7120ce80d4e98a76df90d7
|
|
4
|
+
data.tar.gz: ac86804058048327a006cd3343839299408425dd4e3702fe99aafc571e8e372c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a6a68ed578e07a9d0026f1dd61f3971ddd5f5e6b2ec590e2960644bef88de65cf054c04c2255051e0b550061eaa965a521b5700bfaf139ae5e3d76bf1f1aa19
|
|
7
|
+
data.tar.gz: 36614043d839a6a59c7ecc761f6d47ce8b9ff05cf149a630f33b17cb10b45ed8e374c2d1293c3ef7b9d98324290eeeb9332c2c530aea90642e18878faa08b9c4
|
data/README.md
CHANGED
|
@@ -71,8 +71,25 @@ AUTHENTICATION
|
|
|
71
71
|
|
|
72
72
|
APPS
|
|
73
73
|
deploio apps List all apps
|
|
74
|
+
deploio apps -p PROJECT List apps in a specific project
|
|
75
|
+
deploio apps --chf Show estimated monthly price (CHF) for each app
|
|
74
76
|
deploio apps:info -a APP Show app details
|
|
75
77
|
|
|
78
|
+
PROJECTS
|
|
79
|
+
deploio projects List all projects
|
|
80
|
+
deploio projects --chf Show estimated total price (CHF) for each project
|
|
81
|
+
|
|
82
|
+
BUILDS
|
|
83
|
+
deploio builds List all builds
|
|
84
|
+
deploio builds -a APP List builds for a specific app
|
|
85
|
+
|
|
86
|
+
SERVICES
|
|
87
|
+
deploio services List all services
|
|
88
|
+
deploio services -p PROJECT List services in a specific project
|
|
89
|
+
deploio services -p PROJECT --url List services with connection URLs (requires -p)
|
|
90
|
+
deploio services -p PROJECT --connected-apps Show which apps use each service (requires -p)
|
|
91
|
+
deploio services --chf Show estimated monthly price (CHF) for each service
|
|
92
|
+
|
|
76
93
|
LOGS
|
|
77
94
|
deploio logs -a APP Show recent logs
|
|
78
95
|
deploio logs -a APP --tail Stream logs continuously
|
|
@@ -111,11 +128,58 @@ deploio auth:whoami
|
|
|
111
128
|
# List all apps
|
|
112
129
|
deploio apps
|
|
113
130
|
|
|
131
|
+
# List apps in a specific project
|
|
132
|
+
deploio apps -p myproject
|
|
133
|
+
|
|
134
|
+
# List apps with estimated monthly prices (CHF)
|
|
135
|
+
deploio apps --chf
|
|
136
|
+
|
|
114
137
|
# Show app info
|
|
115
138
|
deploio apps:info -a myproject-staging
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Working with projects
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# List all projects
|
|
145
|
+
deploio projects
|
|
146
|
+
|
|
147
|
+
# List projects with estimated total prices (apps + services)
|
|
148
|
+
deploio projects --chf
|
|
149
|
+
|
|
150
|
+
# Output as JSON
|
|
151
|
+
deploio projects --json
|
|
152
|
+
```
|
|
116
153
|
|
|
117
154
|
```
|
|
118
155
|
|
|
156
|
+
### Working with services
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# List all services
|
|
160
|
+
deploio services
|
|
161
|
+
|
|
162
|
+
# List services in a specific project
|
|
163
|
+
deploio services -p myproject
|
|
164
|
+
|
|
165
|
+
# List services with estimated monthly prices (CHF)
|
|
166
|
+
deploio services --chf
|
|
167
|
+
|
|
168
|
+
# List services with connection URLs (requires --project)
|
|
169
|
+
deploio services -p myproject --url
|
|
170
|
+
|
|
171
|
+
# Show which apps are connected to each service (requires --project)
|
|
172
|
+
deploio services -p myproject --connected-apps
|
|
173
|
+
|
|
174
|
+
# Combine options
|
|
175
|
+
deploio services -p myproject --url --connected-apps --chf
|
|
176
|
+
|
|
177
|
+
# Output as JSON
|
|
178
|
+
deploio services --json
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Note: Prices (--chf) are fetched from the Nine calculator API and cached locally in `~/.deploio/prices.json` for 24 hours.
|
|
182
|
+
|
|
119
183
|
### Logs and execution
|
|
120
184
|
|
|
121
185
|
```bash
|
data/deploio-cli.gemspec
CHANGED
|
@@ -18,6 +18,13 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
19
|
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
20
20
|
|
|
21
|
+
spec.post_install_message = <<~MSG
|
|
22
|
+
To enable shell autocompletion for deploio, add this to your ~/.zshrc:
|
|
23
|
+
|
|
24
|
+
eval "$(deploio completion)"
|
|
25
|
+
|
|
26
|
+
MSG
|
|
27
|
+
|
|
21
28
|
spec.files = Dir.chdir(__dir__) do
|
|
22
29
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
23
30
|
(File.expand_path(f) == __FILE__) ||
|
data/lib/deploio/cli.rb
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "commands/auth"
|
|
5
5
|
require_relative "commands/apps"
|
|
6
|
+
require_relative "commands/builds"
|
|
6
7
|
require_relative "commands/orgs"
|
|
8
|
+
require_relative "commands/projects"
|
|
9
|
+
require_relative "commands/services"
|
|
10
|
+
require_relative "commands/postgresql_backups"
|
|
11
|
+
require_relative "commands/postgresql"
|
|
7
12
|
require_relative "completion_generator"
|
|
8
13
|
|
|
9
14
|
module Deploio
|
|
@@ -40,6 +45,18 @@ module Deploio
|
|
|
40
45
|
desc "orgs COMMAND", "Organization management commands"
|
|
41
46
|
subcommand "orgs", Commands::Orgs
|
|
42
47
|
|
|
48
|
+
desc "projects COMMAND", "Project management commands"
|
|
49
|
+
subcommand "projects", Commands::Projects
|
|
50
|
+
|
|
51
|
+
desc "services COMMAND", "Service management commands"
|
|
52
|
+
subcommand "services", Commands::Services
|
|
53
|
+
|
|
54
|
+
desc "builds COMMAND", "Build management commands"
|
|
55
|
+
subcommand "builds", Commands::Builds
|
|
56
|
+
|
|
57
|
+
desc "pg COMMAND", "PostgreSQL database management commands"
|
|
58
|
+
subcommand "pg", Commands::PostgreSQL
|
|
59
|
+
|
|
43
60
|
# Shortcut for auth:login
|
|
44
61
|
desc "login", "Authenticate with nctl (alias for auth:login)"
|
|
45
62
|
def login
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../price_fetcher"
|
|
4
|
+
|
|
3
5
|
module Deploio
|
|
4
6
|
module Commands
|
|
5
7
|
class Apps < Thor
|
|
@@ -11,10 +13,15 @@ module Deploio
|
|
|
11
13
|
|
|
12
14
|
default_task :list
|
|
13
15
|
|
|
14
|
-
desc "list", "List all
|
|
16
|
+
desc "list", "List apps (all or filtered by project)"
|
|
17
|
+
method_option :project, aliases: "-p", type: :string,
|
|
18
|
+
desc: "Filter by project name"
|
|
19
|
+
method_option :chf, type: :boolean, default: false,
|
|
20
|
+
desc: "Show estimated price (CHF) for each app"
|
|
15
21
|
def list
|
|
16
22
|
setup_options
|
|
17
|
-
|
|
23
|
+
project = merged_options[:project] ? resolve_project(merged_options[:project]) : nil
|
|
24
|
+
raw_apps = project ? @nctl.get_apps_by_project(project) : @nctl.get_all_apps
|
|
18
25
|
|
|
19
26
|
if options[:json]
|
|
20
27
|
puts JSON.pretty_generate(raw_apps)
|
|
@@ -22,11 +29,14 @@ module Deploio
|
|
|
22
29
|
end
|
|
23
30
|
|
|
24
31
|
if raw_apps.empty?
|
|
25
|
-
|
|
32
|
+
msg = project ? "No apps found in project #{merged_options[:project]}" : "No apps found"
|
|
33
|
+
Output.warning(msg) unless merged_options[:dry_run]
|
|
26
34
|
return
|
|
27
35
|
end
|
|
28
36
|
|
|
29
37
|
resolver = AppResolver.new(nctl_client: @nctl)
|
|
38
|
+
show_price = merged_options[:chf]
|
|
39
|
+
price_fetcher = PriceFetcher.new if show_price
|
|
30
40
|
|
|
31
41
|
rows = raw_apps.map do |app|
|
|
32
42
|
metadata = app["metadata"] || {}
|
|
@@ -37,15 +47,24 @@ module Deploio
|
|
|
37
47
|
namespace = metadata["namespace"] || ""
|
|
38
48
|
name = metadata["name"] || ""
|
|
39
49
|
|
|
40
|
-
[
|
|
50
|
+
row = [
|
|
41
51
|
resolver.short_name_for(namespace, name),
|
|
42
52
|
project_from_namespace(namespace, resolver.current_org),
|
|
43
53
|
presence(config["size"], default: "micro"),
|
|
44
54
|
presence(git["revision"])
|
|
45
55
|
]
|
|
56
|
+
|
|
57
|
+
if show_price
|
|
58
|
+
price = price_fetcher.price_for_app(app)
|
|
59
|
+
row << price_fetcher.format_price(price)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
row
|
|
46
63
|
end
|
|
47
64
|
|
|
48
|
-
|
|
65
|
+
headers = %w[APP PROJECT SIZE REVISION]
|
|
66
|
+
headers << "PRICE" if show_price
|
|
67
|
+
Output.table(rows, headers: headers)
|
|
49
68
|
end
|
|
50
69
|
|
|
51
70
|
desc "info", "Show app details"
|
|
@@ -93,10 +112,13 @@ module Deploio
|
|
|
93
112
|
ready_condition = conditions.find { |c| c["type"] == "Ready" }
|
|
94
113
|
synced_condition = conditions.find { |c| c["type"] == "Synced" }
|
|
95
114
|
|
|
115
|
+
default_url = at_provider["defaultURL"]
|
|
116
|
+
default_url_display = default_url ? Output.link(default_url) : "-"
|
|
117
|
+
|
|
96
118
|
Output.table([
|
|
97
119
|
["Ready", presence(ready_condition&.dig("status"))],
|
|
98
120
|
["Synced", presence(synced_condition&.dig("status"))],
|
|
99
|
-
["Default URL",
|
|
121
|
+
["Default URL", default_url_display],
|
|
100
122
|
["Latest Build", presence(at_provider["latestBuild"])],
|
|
101
123
|
["Latest Release", presence(at_provider["latestRelease"])]
|
|
102
124
|
])
|
|
@@ -106,7 +128,7 @@ module Deploio
|
|
|
106
128
|
hosts = for_provider["hosts"] || []
|
|
107
129
|
if hosts.any?
|
|
108
130
|
Output.header("Hosts")
|
|
109
|
-
Output.
|
|
131
|
+
Output.list(hosts.map { |h| Output.link(h, "https://#{h}") })
|
|
110
132
|
puts
|
|
111
133
|
end
|
|
112
134
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Deploio
|
|
6
|
+
module Commands
|
|
7
|
+
class Builds < Thor
|
|
8
|
+
include SharedOptions
|
|
9
|
+
|
|
10
|
+
namespace "builds"
|
|
11
|
+
|
|
12
|
+
class_option :json, type: :boolean, default: false, desc: "Output as JSON"
|
|
13
|
+
|
|
14
|
+
desc "list", "List builds (all or for a specific app)"
|
|
15
|
+
def list
|
|
16
|
+
setup_options
|
|
17
|
+
|
|
18
|
+
if merged_options[:app]
|
|
19
|
+
list_for_app
|
|
20
|
+
else
|
|
21
|
+
list_all
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "logs [BUILD_NAME]", "Show build logs"
|
|
26
|
+
method_option :tail, aliases: "-t", type: :boolean, default: false, desc: "Stream logs continuously"
|
|
27
|
+
method_option :lines, aliases: "-n", type: :numeric, default: 5000, desc: "Number of lines to show"
|
|
28
|
+
def logs(build_name = nil)
|
|
29
|
+
setup_options
|
|
30
|
+
app_ref = merged_options[:app] ? resolve_app : nil
|
|
31
|
+
@nctl.build_logs(build_name, app_ref: app_ref, tail: options[:tail], lines: options[:lines])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def list_all
|
|
37
|
+
raw_builds = @nctl.get_all_builds
|
|
38
|
+
|
|
39
|
+
if options[:json]
|
|
40
|
+
puts JSON.pretty_generate(raw_builds)
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if raw_builds.empty?
|
|
45
|
+
Output.warning("No builds found") unless merged_options[:dry_run]
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
resolver = AppResolver.new(nctl_client: @nctl)
|
|
50
|
+
|
|
51
|
+
rows = raw_builds.map do |build|
|
|
52
|
+
metadata = build["metadata"] || {}
|
|
53
|
+
spec = build["spec"] || {}
|
|
54
|
+
status = build["status"] || {}
|
|
55
|
+
for_provider = spec["forProvider"] || {}
|
|
56
|
+
git = for_provider["sourceConfig"]&.dig("git") || {}
|
|
57
|
+
at_provider = status["atProvider"] || {}
|
|
58
|
+
labels = metadata["labels"] || {}
|
|
59
|
+
app_name = labels["application.apps.nine.ch/name"] || "-"
|
|
60
|
+
namespace = metadata["namespace"] || ""
|
|
61
|
+
|
|
62
|
+
[
|
|
63
|
+
resolver.short_name_for(namespace, app_name),
|
|
64
|
+
presence(metadata["name"]),
|
|
65
|
+
format_status(at_provider["buildStatus"]),
|
|
66
|
+
presence(git["revision"], max_length: 20),
|
|
67
|
+
format_timestamp(metadata["creationTimestamp"])
|
|
68
|
+
]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Output.table(rows, headers: %w[APP BUILD STATUS REVISION CREATED])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def list_for_app
|
|
75
|
+
app_ref = resolve_app
|
|
76
|
+
raw_builds = @nctl.get_builds(app_ref)
|
|
77
|
+
|
|
78
|
+
if options[:json]
|
|
79
|
+
puts JSON.pretty_generate(raw_builds)
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if raw_builds.empty?
|
|
84
|
+
Output.warning("No builds found for #{app_ref.full_name}") unless merged_options[:dry_run]
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
rows = raw_builds.map do |build|
|
|
89
|
+
metadata = build["metadata"] || {}
|
|
90
|
+
spec = build["spec"] || {}
|
|
91
|
+
status = build["status"] || {}
|
|
92
|
+
for_provider = spec["forProvider"] || {}
|
|
93
|
+
git = for_provider["sourceConfig"]&.dig("git") || {}
|
|
94
|
+
at_provider = status["atProvider"] || {}
|
|
95
|
+
|
|
96
|
+
[
|
|
97
|
+
presence(metadata["name"]),
|
|
98
|
+
format_status(at_provider["buildStatus"]),
|
|
99
|
+
presence(git["revision"], max_length: 30),
|
|
100
|
+
format_timestamp(metadata["creationTimestamp"])
|
|
101
|
+
]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
Output.table(rows, headers: %w[BUILD STATUS REVISION CREATED])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def presence(value, default: "-", max_length: nil)
|
|
108
|
+
return default if value.nil? || value.to_s.empty?
|
|
109
|
+
|
|
110
|
+
str = value.to_s
|
|
111
|
+
(max_length && str.length > max_length) ? "#{str[0, max_length - 3]}..." : str
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def format_status(status)
|
|
115
|
+
return "-" if status.nil? || status.to_s.empty?
|
|
116
|
+
|
|
117
|
+
case status.downcase
|
|
118
|
+
when "succeeded", "success"
|
|
119
|
+
Output.color_enabled ? "\e[32m#{status}\e[0m" : status
|
|
120
|
+
when "error", "failed"
|
|
121
|
+
Output.color_enabled ? "\e[31m#{status}\e[0m" : status
|
|
122
|
+
when "building", "running", "pending"
|
|
123
|
+
Output.color_enabled ? "\e[33m#{status}\e[0m" : status
|
|
124
|
+
else
|
|
125
|
+
status
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def format_timestamp(timestamp)
|
|
130
|
+
return "-" if timestamp.nil? || timestamp.to_s.empty?
|
|
131
|
+
|
|
132
|
+
time = Time.parse(timestamp)
|
|
133
|
+
time.strftime("%Y-%m-%d %H:%M")
|
|
134
|
+
rescue ArgumentError
|
|
135
|
+
timestamp
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
module Deploio
|
|
2
|
+
module Commands
|
|
3
|
+
class PostgreSQL < Thor
|
|
4
|
+
include SharedOptions
|
|
5
|
+
|
|
6
|
+
namespace "pg"
|
|
7
|
+
|
|
8
|
+
class_option :json, type: :boolean, default: false, desc: "Output as JSON"
|
|
9
|
+
|
|
10
|
+
default_task :list
|
|
11
|
+
|
|
12
|
+
desc "list", "List all PostgreSQL databases"
|
|
13
|
+
def list
|
|
14
|
+
setup_options
|
|
15
|
+
raw_dbs = @nctl.get_all_pg_databases
|
|
16
|
+
|
|
17
|
+
if options[:json]
|
|
18
|
+
puts JSON.pretty_generate(raw_dbs)
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if raw_dbs.empty?
|
|
23
|
+
Output.warning("No PostgreSQL databases found") unless merged_options[:dry_run]
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
resolver = PgDatabaseResolver.new(nctl_client: @nctl)
|
|
28
|
+
|
|
29
|
+
rows = raw_dbs.map do |pg|
|
|
30
|
+
kind = pg["kind"] || ""
|
|
31
|
+
metadata = pg["metadata"] || {}
|
|
32
|
+
spec = pg["spec"] || {}
|
|
33
|
+
for_provider = spec["forProvider"] || {}
|
|
34
|
+
version = for_provider["version"]
|
|
35
|
+
namespace = metadata["namespace"] || ""
|
|
36
|
+
name = metadata["name"] || ""
|
|
37
|
+
|
|
38
|
+
[
|
|
39
|
+
resolver.short_name_for(namespace, name),
|
|
40
|
+
project_from_namespace(namespace, resolver.current_org),
|
|
41
|
+
presence(kind, default: "-"),
|
|
42
|
+
presence(version, default: "?")
|
|
43
|
+
]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
Output.table(rows, headers: ["NAME", "PROJECT", "KIND", "VERSION"])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc "info NAME", "Show PostgreSQL database details"
|
|
50
|
+
def info(name)
|
|
51
|
+
setup_options
|
|
52
|
+
resolver = PgDatabaseResolver.new(nctl_client: @nctl)
|
|
53
|
+
db_ref = resolver.resolve(database_name: name)
|
|
54
|
+
data = @nctl.get_pg_database(db_ref)
|
|
55
|
+
|
|
56
|
+
if options[:json]
|
|
57
|
+
puts JSON.pretty_generate(data)
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
kind = data["kind"]
|
|
62
|
+
metadata = data["metadata"] || {}
|
|
63
|
+
spec = data["spec"] || {}
|
|
64
|
+
for_provider = spec["forProvider"] || {}
|
|
65
|
+
status = data["status"] || {}
|
|
66
|
+
at_provider = status["atProvider"] || {}
|
|
67
|
+
|
|
68
|
+
Output.header("PostgreSQL Database: #{db_ref.full_name}")
|
|
69
|
+
puts
|
|
70
|
+
|
|
71
|
+
Output.header("General")
|
|
72
|
+
Output.table([
|
|
73
|
+
["Name", presence(metadata["name"])],
|
|
74
|
+
["Project", presence(metadata["namespace"])],
|
|
75
|
+
["Kind", presence(kind, default: "-")],
|
|
76
|
+
["Version", presence(for_provider["version"], default: "?")],
|
|
77
|
+
["FQDN", presence(at_provider["fqdn"])],
|
|
78
|
+
["Size", presence(at_provider["size"], default: "-")]
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
puts
|
|
82
|
+
|
|
83
|
+
Output.header("Status")
|
|
84
|
+
conditions = status["conditions"] || []
|
|
85
|
+
ready_condition = conditions.find { |c| c["type"] == "Ready" }
|
|
86
|
+
synced_condition = conditions.find { |c| c["type"] == "Synced" }
|
|
87
|
+
|
|
88
|
+
Output.table([
|
|
89
|
+
["Ready", presence(ready_condition&.dig("status"))],
|
|
90
|
+
["Synced", presence(synced_condition&.dig("status"))]
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
if for_provider["allowedCIDRs"].is_a?(Array)
|
|
94
|
+
puts
|
|
95
|
+
|
|
96
|
+
Output.header("Access")
|
|
97
|
+
Output.table([
|
|
98
|
+
["Allowed CIDRs", for_provider["allowedCIDRs"].join(", ")]
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
puts
|
|
102
|
+
|
|
103
|
+
Output.header("SSH Keys")
|
|
104
|
+
for_provider["sshKeys"].each do |key|
|
|
105
|
+
puts "- #{key}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
rescue Deploio::Error => e
|
|
109
|
+
Output.error(e.message)
|
|
110
|
+
exit 1
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
desc "backups COMMAND", "Manage PostgreSQL database backups"
|
|
114
|
+
subcommand "backups", Commands::PostgreSQLBackups
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def presence(value, default: "-")
|
|
119
|
+
(value.nil? || value.to_s.empty?) ? default : value
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def project_from_namespace(namespace, current_org)
|
|
123
|
+
if current_org && namespace.start_with?("#{current_org}-")
|
|
124
|
+
namespace.delete_prefix("#{current_org}-")
|
|
125
|
+
else
|
|
126
|
+
namespace
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Deploio
|
|
2
|
+
module Commands
|
|
3
|
+
class PostgreSQLBackups < Thor
|
|
4
|
+
include SharedOptions
|
|
5
|
+
|
|
6
|
+
namespace "pg:backups"
|
|
7
|
+
|
|
8
|
+
desc "capture NAME", "Capture a new backup for the specified PostgreSQL database"
|
|
9
|
+
def capture(name)
|
|
10
|
+
setup_options
|
|
11
|
+
resolver = PgDatabaseResolver.new(nctl_client: @nctl)
|
|
12
|
+
db_ref = resolver.resolve(database_name: name)
|
|
13
|
+
data = @nctl.get_pg_database(db_ref)
|
|
14
|
+
kind = data["kind"] || ""
|
|
15
|
+
|
|
16
|
+
unless kind == "Postgres" || @nctl.dry_run
|
|
17
|
+
Output.error("Backups can only be captured for PostgreSQL databases. (shared dbs are not supported)")
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
fqdn = data.dig("status", "atProvider", "fqdn")
|
|
22
|
+
if fqdn.nil? || fqdn.empty?
|
|
23
|
+
Output.error("Database FQDN not found; cannot capture backup.")
|
|
24
|
+
exit 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
cmd = ["ssh", "dbadmin@#{fqdn}", "sudo nine-postgresql-backup"]
|
|
28
|
+
Output.command(cmd.join(" "))
|
|
29
|
+
system(*cmd) unless @nctl.dry_run
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc "download NAME [--output destination_path]", "Download the latest backup for the specified PostgreSQL database instance"
|
|
33
|
+
method_option :output, type: :string, desc: "Output file path (defaults to current directory with auto-generated name)"
|
|
34
|
+
method_option :db_name, type: :string, desc: "If there are multiple DBs, specify which one to download the backup for", default: nil
|
|
35
|
+
def download(name)
|
|
36
|
+
destination = options[:output] || "./#{name}-latest-backup.zst"
|
|
37
|
+
|
|
38
|
+
setup_options
|
|
39
|
+
resolver = PgDatabaseResolver.new(nctl_client: @nctl)
|
|
40
|
+
db_ref = resolver.resolve(database_name: name)
|
|
41
|
+
data = @nctl.get_pg_database(db_ref)
|
|
42
|
+
kind = data["kind"] || ""
|
|
43
|
+
|
|
44
|
+
unless kind == "Postgres" || @nctl.dry_run
|
|
45
|
+
Output.error("Backups can only be downloaded for PostgreSQL databases. (shared dbs are not supported)")
|
|
46
|
+
exit 1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
databases = data.dig("status", "atProvider", "databases")&.keys || []
|
|
50
|
+
databases.reject! { |db| db.strip.empty? }
|
|
51
|
+
if databases.empty?
|
|
52
|
+
Output.error("No databases found in PostgreSQL instance; cannot download backup.")
|
|
53
|
+
exit 1
|
|
54
|
+
elsif databases.size > 1 && options[:db_name].nil?
|
|
55
|
+
Output.error("Multiple databases found in PostgreSQL instance")
|
|
56
|
+
Output.error("Databases: #{databases.join(", ")}")
|
|
57
|
+
Output.error("Please specify the database name using the --db_name option.")
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
db_name = options[:db_name] || databases.first
|
|
62
|
+
|
|
63
|
+
fqdn = data.dig("status", "atProvider", "fqdn")
|
|
64
|
+
if fqdn.nil? || fqdn.empty?
|
|
65
|
+
Output.error("Database FQDN not found; cannot download backup.")
|
|
66
|
+
exit 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
cmd = ["rsync", "-avz", "dbadmin@#{fqdn}:~/backup/postgresql/latest/customer/#{db_name}/#{db_name}.zst", destination]
|
|
70
|
+
Output.command(cmd.join(" "))
|
|
71
|
+
system(*cmd) unless @nctl.dry_run
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|