deploio-cli 0.1.1 → 0.3.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 +73 -1
- data/lib/deploio/cli.rb +19 -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 +96 -0
- data/lib/deploio/commands/projects.rb +185 -0
- data/lib/deploio/commands/services.rb +186 -0
- data/lib/deploio/completion_generator.rb +22 -3
- data/lib/deploio/nctl_client.rb +205 -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/postgres_backup_service.rb +68 -0
- data/lib/deploio/postgres_database_backup_service.rb +116 -0
- data/lib/deploio/price_fetcher.rb +201 -0
- data/lib/deploio/rclone_client.rb +99 -0
- data/lib/deploio/shared_options.rb +18 -2
- data/lib/deploio/templates/completion.zsh.erb +134 -1
- data/lib/deploio/version.rb +1 -1
- data/lib/deploio.rb +9 -0
- metadata +13 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ef7a08d44a816dd88331bf7e007a536ef399b10afbf05229bef8e1e3137629e
|
|
4
|
+
data.tar.gz: d042f89dc44e64df49d599f1291f88151af45461132156af5c36486fa6408baa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 603238b92b6b23a32bf92f81a0a89a4cc0edbf8daad40519f1f165c095629ccb5f83775f01f0aae67541f6177f845ebc9797584fdcdb977425fa2307c2d8f98e
|
|
7
|
+
data.tar.gz: 2ac16607435cc562bcf3d3e94066002315bb06f198122fe6d088ee25782f9153106ab065e217f34a86d5aa99a4cb2d7d154876eabcc958266f2e74cc6771ef09
|
data/README.md
CHANGED
|
@@ -6,6 +6,7 @@ A CLI for [Deploio](https://www.deplo.io/) that wraps [`nctl`](https://github.co
|
|
|
6
6
|
|
|
7
7
|
- Ruby 3.3+
|
|
8
8
|
- nctl version 1.10.0 or higher
|
|
9
|
+
- [rclone](https://rclone.org/) (`brew install rclone`) — only needed for `deploio pg backups` on economy-tier databases
|
|
9
10
|
|
|
10
11
|
## Installation
|
|
11
12
|
|
|
@@ -18,7 +19,7 @@ bundle install
|
|
|
18
19
|
bundle exec bin/deploio --help
|
|
19
20
|
```
|
|
20
21
|
|
|
21
|
-
### As a gem
|
|
22
|
+
### As a gem
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
25
|
gem install deploio-cli
|
|
@@ -71,8 +72,32 @@ AUTHENTICATION
|
|
|
71
72
|
|
|
72
73
|
APPS
|
|
73
74
|
deploio apps List all apps
|
|
75
|
+
deploio apps -p PROJECT List apps in a specific project
|
|
76
|
+
deploio apps --chf Show estimated monthly price (CHF) for each app
|
|
74
77
|
deploio apps:info -a APP Show app details
|
|
75
78
|
|
|
79
|
+
PROJECTS
|
|
80
|
+
deploio projects List all projects
|
|
81
|
+
deploio projects --chf Show estimated total price (CHF) for each project
|
|
82
|
+
|
|
83
|
+
BUILDS
|
|
84
|
+
deploio builds List all builds
|
|
85
|
+
deploio builds -a APP List builds for a specific app
|
|
86
|
+
|
|
87
|
+
SERVICES
|
|
88
|
+
deploio services List all services
|
|
89
|
+
deploio services -p PROJECT List services in a specific project
|
|
90
|
+
deploio services -p PROJECT --url List services with connection URLs (requires -p)
|
|
91
|
+
deploio services -p PROJECT --connected-apps Show which apps use each service (requires -p)
|
|
92
|
+
deploio services --chf Show estimated monthly price (CHF) for each service
|
|
93
|
+
|
|
94
|
+
POSTGRESQL
|
|
95
|
+
deploio pg List all PostgreSQL databases
|
|
96
|
+
deploio pg:info NAME Show database details
|
|
97
|
+
deploio pg backups list NAME List available backups
|
|
98
|
+
deploio pg backups download NAME Download the latest backup
|
|
99
|
+
deploio pg backups capture NAME Capture a new backup
|
|
100
|
+
|
|
76
101
|
LOGS
|
|
77
102
|
deploio logs -a APP Show recent logs
|
|
78
103
|
deploio logs -a APP --tail Stream logs continuously
|
|
@@ -111,11 +136,58 @@ deploio auth:whoami
|
|
|
111
136
|
# List all apps
|
|
112
137
|
deploio apps
|
|
113
138
|
|
|
139
|
+
# List apps in a specific project
|
|
140
|
+
deploio apps -p myproject
|
|
141
|
+
|
|
142
|
+
# List apps with estimated monthly prices (CHF)
|
|
143
|
+
deploio apps --chf
|
|
144
|
+
|
|
114
145
|
# Show app info
|
|
115
146
|
deploio apps:info -a myproject-staging
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Working with projects
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# List all projects
|
|
153
|
+
deploio projects
|
|
154
|
+
|
|
155
|
+
# List projects with estimated total prices (apps + services)
|
|
156
|
+
deploio projects --chf
|
|
157
|
+
|
|
158
|
+
# Output as JSON
|
|
159
|
+
deploio projects --json
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Working with services
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# List all services
|
|
168
|
+
deploio services
|
|
116
169
|
|
|
170
|
+
# List services in a specific project
|
|
171
|
+
deploio services -p myproject
|
|
172
|
+
|
|
173
|
+
# List services with estimated monthly prices (CHF)
|
|
174
|
+
deploio services --chf
|
|
175
|
+
|
|
176
|
+
# List services with connection URLs (requires --project)
|
|
177
|
+
deploio services -p myproject --url
|
|
178
|
+
|
|
179
|
+
# Show which apps are connected to each service (requires --project)
|
|
180
|
+
deploio services -p myproject --connected-apps
|
|
181
|
+
|
|
182
|
+
# Combine options
|
|
183
|
+
deploio services -p myproject --url --connected-apps --chf
|
|
184
|
+
|
|
185
|
+
# Output as JSON
|
|
186
|
+
deploio services --json
|
|
117
187
|
```
|
|
118
188
|
|
|
189
|
+
Note: Prices (--chf) are fetched from the Nine calculator API and cached locally in `~/.deploio/prices.json` for 24 hours.
|
|
190
|
+
|
|
119
191
|
### Logs and execution
|
|
120
192
|
|
|
121
193
|
```bash
|
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
|
|
@@ -83,6 +100,8 @@ module Deploio
|
|
|
83
100
|
|
|
84
101
|
private
|
|
85
102
|
|
|
103
|
+
# The aliases above hand off with Thor.start, which starts from a fresh
|
|
104
|
+
# option parse, so the shared class options have to be passed as arguments.
|
|
86
105
|
def build_option_args
|
|
87
106
|
args = []
|
|
88
107
|
args << "--dry-run" if options[:dry_run]
|
|
@@ -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,96 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module Deploio
|
|
4
|
+
module Commands
|
|
5
|
+
class PostgreSQLBackups < Thor
|
|
6
|
+
include SharedOptions
|
|
7
|
+
|
|
8
|
+
namespace "pg:backups"
|
|
9
|
+
|
|
10
|
+
DEDICATED_KIND = "Postgres"
|
|
11
|
+
ECONOMY_KIND = "PostgresDatabase"
|
|
12
|
+
|
|
13
|
+
desc "capture NAME", "Capture a new backup for the specified PostgreSQL database"
|
|
14
|
+
def capture(name)
|
|
15
|
+
backup_service_for(name).capture
|
|
16
|
+
rescue Deploio::Error => e
|
|
17
|
+
Output.error(e.message)
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc "list NAME", "List the available backups for the specified PostgreSQL database"
|
|
22
|
+
def list(name)
|
|
23
|
+
backups = backup_service_for(name).backups
|
|
24
|
+
if backups.empty?
|
|
25
|
+
Output.warning("No backups found for '#{name}'")
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
rows = backups.map do |backup|
|
|
30
|
+
[format_time(backup["ModTime"]), format_size(backup["Size"]), backup["Name"]]
|
|
31
|
+
end
|
|
32
|
+
Output.table(rows, headers: ["DATE", "SIZE", "NAME"])
|
|
33
|
+
rescue Deploio::Error => e
|
|
34
|
+
Output.error(e.message)
|
|
35
|
+
exit 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
desc "download NAME [--output destination_path]", "Download the latest backup for the specified PostgreSQL database instance"
|
|
39
|
+
method_option :output, type: :string, desc: "Output file path (defaults to current directory with auto-generated name)"
|
|
40
|
+
method_option :db_name, type: :string, desc: "If there are multiple DBs, specify which one to download the backup for", default: nil
|
|
41
|
+
def download(name)
|
|
42
|
+
service = backup_service_for(name)
|
|
43
|
+
destination = merged_options[:output] || service.default_destination
|
|
44
|
+
backup = service.download(destination: destination, db_name: merged_options[:db_name])
|
|
45
|
+
|
|
46
|
+
# Only the economy tier knows when its backup was taken.
|
|
47
|
+
if backup
|
|
48
|
+
Output.success("Downloaded backup from #{format_time(backup["ModTime"])} to #{destination}")
|
|
49
|
+
else
|
|
50
|
+
Output.success("Downloaded backup to #{destination}")
|
|
51
|
+
end
|
|
52
|
+
rescue Deploio::Error => e
|
|
53
|
+
Output.error(e.message)
|
|
54
|
+
exit 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def backup_service_for(name)
|
|
60
|
+
setup_options
|
|
61
|
+
resolver = PgDatabaseResolver.new(nctl_client: @nctl)
|
|
62
|
+
db_ref = resolver.resolve(database_name: name)
|
|
63
|
+
data = @nctl.get_pg_database(db_ref)
|
|
64
|
+
raise Deploio::Error, "Could not read database '#{db_ref.full_name}'." if data.nil?
|
|
65
|
+
|
|
66
|
+
case data["kind"]
|
|
67
|
+
when DEDICATED_KIND
|
|
68
|
+
PostgresBackupService.new(data: data, name: name, dry_run: @nctl.dry_run)
|
|
69
|
+
when ECONOMY_KIND
|
|
70
|
+
PostgresDatabaseBackupService.new(db_ref: db_ref, data: data, nctl_client: @nctl, name: name)
|
|
71
|
+
else
|
|
72
|
+
raise Deploio::UnsupportedBackupOperationError,
|
|
73
|
+
"Backups are not supported for databases of kind '#{data["kind"]}'."
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def format_time(value)
|
|
78
|
+
Time.parse(value.to_s).localtime.strftime("%Y-%m-%d %H:%M")
|
|
79
|
+
rescue ArgumentError, TypeError
|
|
80
|
+
value.to_s
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def format_size(bytes)
|
|
84
|
+
bytes = bytes.to_i
|
|
85
|
+
units = ["B", "KiB", "MiB", "GiB", "TiB"]
|
|
86
|
+
index = 0
|
|
87
|
+
size = bytes.to_f
|
|
88
|
+
while size >= 1024 && index < units.size - 1
|
|
89
|
+
size /= 1024
|
|
90
|
+
index += 1
|
|
91
|
+
end
|
|
92
|
+
(index.zero? ? "#{bytes} B" : format("%.1f %s", size, units[index]))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|