renuo-cli 4.27.0 → 4.29.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/lib/renuo/cli/commands/configure_semaphore.rb +3 -2
- data/lib/renuo/cli/commands/release.rb +3 -1
- data/lib/renuo/cli/commands/review_app/deploy.rb +114 -0
- data/lib/renuo/cli/commands/review_app/name.rb +38 -0
- data/lib/renuo/cli/commands/review_app/purge.rb +67 -0
- data/lib/renuo/cli/commands/review_app/setup_semaphoreci.rb +87 -0
- data/lib/renuo/cli/commands/review_app/start.rb +53 -0
- data/lib/renuo/cli/helpers/actor_contracts.rb +16 -0
- data/lib/renuo/cli/helpers/cmd_trail.rb +45 -0
- data/lib/renuo/cli/helpers/review_app_helper.rb +46 -0
- data/lib/renuo/cli/helpers/safe_review_app_name_contract.rb +17 -0
- data/lib/renuo/cli/services/deploio/copy_app.rb +55 -0
- data/lib/renuo/cli/services/deploio/copy_database_data.rb +57 -0
- data/lib/renuo/cli/services/deploio/copy_s3_data.rb +70 -0
- data/lib/renuo/cli/services/deploio/create_database.rb +49 -0
- data/lib/renuo/cli/services/deploio/create_kvs.rb +41 -0
- data/lib/renuo/cli/services/deploio/create_s3.rb +82 -0
- data/lib/renuo/cli/services/deploio/inventorize_origin_resources.rb +174 -0
- data/lib/renuo/cli/services/deploio/start_target_app.rb +19 -0
- data/lib/renuo/cli/services/deploio/update_target_app_env.rb +37 -0
- data/lib/renuo/cli/services/deploio.rb +16 -4
- data/lib/renuo/cli/templates/semaphore/review-app-deploy.yml.erb +44 -0
- data/lib/renuo/cli/templates/semaphore/review-app-purge.yml.erb +21 -0
- data/lib/renuo/cli/version.rb +1 -1
- data/lib/renuo/cli.rb +10 -0
- metadata +50 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "create_s3"
|
|
4
|
+
|
|
5
|
+
class Renuo::Cli::Services::Deploio
|
|
6
|
+
class CopyS3Data < Actor
|
|
7
|
+
include ReviewAppHelper
|
|
8
|
+
|
|
9
|
+
DATA_DIR = File.join(Dir.tmpdir, "review-app-bucket-data")
|
|
10
|
+
LISTING_ATTEMPTS = 24
|
|
11
|
+
LISTING_INTERVAL_IN_SECONDS = 5
|
|
12
|
+
|
|
13
|
+
input :target_app_name, must: satisfy(SafeReviewAppNameContract)
|
|
14
|
+
input :s3_env
|
|
15
|
+
input :origin_bucket_name
|
|
16
|
+
input :origin_bucket_endpoint
|
|
17
|
+
input :origin_bucket_access_key
|
|
18
|
+
input :origin_bucket_secret_key
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
return puts "bucket #{target_app_name} already holds objects, skipping data copy" unless bucket_empty?
|
|
22
|
+
|
|
23
|
+
puts "copying bucket data from #{origin_bucket_name} into #{target_app_name}..."
|
|
24
|
+
download_data
|
|
25
|
+
upload_data
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def download_data
|
|
31
|
+
system! "aws s3 sync s3://#{origin_bucket_name} #{DATA_DIR} --endpoint-url https://#{origin_bucket_endpoint}",
|
|
32
|
+
aws_credentials(origin_bucket_access_key, origin_bucket_secret_key)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# aws exits non-zero on some empty listings, so only stderr tells a failure apart.
|
|
36
|
+
def bucket_empty?
|
|
37
|
+
stdout, stderr = list_target_bucket
|
|
38
|
+
abort "'aws s3' failed: #{stderr}" unless stderr.strip.empty?
|
|
39
|
+
stdout.strip.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Deploio reports a fresh bucket ready seconds before its permissions grant access.
|
|
43
|
+
def list_target_bucket
|
|
44
|
+
(1..LISTING_ATTEMPTS).each do |attempt|
|
|
45
|
+
stdout, stderr, = system!("aws s3 ls s3://#{target_app_name} --endpoint-url #{target_endpoint}",
|
|
46
|
+
target_credentials, quiet: true, abort_on_failure: false)
|
|
47
|
+
return [stdout, stderr] if attempt == LISTING_ATTEMPTS || stderr.exclude?("AccessDenied")
|
|
48
|
+
|
|
49
|
+
puts "waiting for access to bucket #{target_app_name}..."
|
|
50
|
+
Kernel.sleep(LISTING_INTERVAL_IN_SECONDS)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def upload_data
|
|
55
|
+
system! "aws s3 sync #{DATA_DIR} s3://#{target_app_name} --endpoint-url #{target_endpoint}", target_credentials
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def target_endpoint
|
|
59
|
+
s3_env.fetch(target_schema[:endpoint])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def target_credentials
|
|
63
|
+
aws_credentials(s3_env.fetch(target_schema[:access_key]), s3_env.fetch(target_schema[:secret_key]))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def target_schema
|
|
67
|
+
CreateS3::ENV_SCHEMAS_IN_THE_WILD.find { |candidate| s3_env.key?(candidate[:secret_key]) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
class Renuo::Cli::Services::Deploio
|
|
6
|
+
class CreateDatabase < Actor
|
|
7
|
+
include ReviewAppHelper
|
|
8
|
+
include Renuo::Cli::Services
|
|
9
|
+
|
|
10
|
+
input :project
|
|
11
|
+
input :target_app_name, must: satisfy(SafeReviewAppNameContract)
|
|
12
|
+
input :origin_database_location, allow_nil: true, default: nil
|
|
13
|
+
input :origin_database_version, allow_nil: true, default: nil
|
|
14
|
+
input :origin_database_collation, allow_nil: true, default: nil
|
|
15
|
+
|
|
16
|
+
output :database_env
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
if Deploio.resource_exists?("postgresdatabase", target_app_name, project)
|
|
20
|
+
puts "database #{target_app_name} already exists, skipping creation"
|
|
21
|
+
else
|
|
22
|
+
create_database
|
|
23
|
+
end
|
|
24
|
+
self.database_env = { "DATABASE_URL" => database_url }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def create_database
|
|
30
|
+
puts "creating database #{target_app_name}..."
|
|
31
|
+
system! ["nctl create postgresdatabase #{target_app_name} -p #{project}",
|
|
32
|
+
*origin_flags,
|
|
33
|
+
"--backup-schedule=disabled --wait"].join(" ")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def origin_flags
|
|
37
|
+
{ "--location" => origin_database_location,
|
|
38
|
+
"--postgres-database-version" => origin_database_version,
|
|
39
|
+
"--collation" => origin_database_collation }
|
|
40
|
+
.filter_map { |flag, value| "#{flag} #{Shellwords.escape(value)}" if value }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def database_url
|
|
44
|
+
stdout, = system!("nctl get postgresdatabase #{target_app_name} -p #{project} --print-connection-string",
|
|
45
|
+
quiet: true)
|
|
46
|
+
stdout.strip
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
class Renuo::Cli::Services::Deploio
|
|
6
|
+
class CreateKvs < Actor
|
|
7
|
+
include ReviewAppHelper
|
|
8
|
+
include Renuo::Cli::Services
|
|
9
|
+
|
|
10
|
+
input :project
|
|
11
|
+
input :target_app_name, must: satisfy(SafeReviewAppNameContract)
|
|
12
|
+
input :origin_key_value_store_location, allow_nil: true, default: nil
|
|
13
|
+
|
|
14
|
+
output :kvs_env
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
if Deploio.resource_exists?("kvs", target_app_name, project)
|
|
18
|
+
puts "key-value store #{target_app_name} already exists, skipping creation"
|
|
19
|
+
else
|
|
20
|
+
create_kvs
|
|
21
|
+
end
|
|
22
|
+
self.kvs_env = { "REDIS_URL" => redis_url }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def create_kvs
|
|
28
|
+
system! ["nctl create kvs #{target_app_name} -p #{project}", location_flag, "--wait"].compact.join(" ")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def location_flag
|
|
32
|
+
"--location #{Shellwords.escape(origin_key_value_store_location)}" if origin_key_value_store_location
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def redis_url
|
|
36
|
+
token, = system!("nctl get kvs #{target_app_name} -p #{project} --print-token", quiet: true)
|
|
37
|
+
fqdn = Deploio.fetch_resource("kvs", target_app_name, project).dig("status", "atProvider", "fqdn")
|
|
38
|
+
"rediss://:#{token.strip}@#{fqdn}:6379"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Renuo::Cli::Services::Deploio
|
|
4
|
+
class CreateS3 < Actor
|
|
5
|
+
include ReviewAppHelper
|
|
6
|
+
include Renuo::Cli::Services
|
|
7
|
+
|
|
8
|
+
DEFAULT_BUCKET_LOCATION = "nine-es34"
|
|
9
|
+
ENV_SCHEMAS_IN_THE_WILD = [
|
|
10
|
+
{
|
|
11
|
+
name: "DEPLOIO_BUCKET_NAME",
|
|
12
|
+
endpoint: "DEPLOIO_BUCKET_ENDPOINT",
|
|
13
|
+
access_key: "DEPLOIO_BUCKET_ACCESS_KEY",
|
|
14
|
+
secret_key: "DEPLOIO_BUCKET_SECRET_KEY"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "DEPLOIO_BUCKET",
|
|
18
|
+
endpoint: "DEPLOIO_ENDPOINT",
|
|
19
|
+
access_key: "DEPLOIO_ACCESS_KEY",
|
|
20
|
+
secret_key: "DEPLOIO_SECRET_KEY"
|
|
21
|
+
}
|
|
22
|
+
].freeze
|
|
23
|
+
SENSITIVE_ENV_KEYS = ENV_SCHEMAS_IN_THE_WILD.flat_map { |schema| schema.values_at(:access_key, :secret_key) }.freeze
|
|
24
|
+
|
|
25
|
+
input :project
|
|
26
|
+
input :target_app_name, must: satisfy(SafeReviewAppNameContract)
|
|
27
|
+
input :template, default: {}
|
|
28
|
+
|
|
29
|
+
output :s3_env
|
|
30
|
+
|
|
31
|
+
def call
|
|
32
|
+
location = template[:bucket_location] || DEFAULT_BUCKET_LOCATION
|
|
33
|
+
create_bucket_user(location)
|
|
34
|
+
create_bucket(location)
|
|
35
|
+
self.s3_env = bucket_env
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def app_env?(key)
|
|
41
|
+
(template[:app_env] || []).any? { |variable| variable["name"] == key }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create_bucket_user(location)
|
|
45
|
+
return if Deploio.resource_exists?("bucketuser", target_app_name, project)
|
|
46
|
+
|
|
47
|
+
system! "nctl create bucketuser #{target_app_name} -p #{project} --location #{location} --wait"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def create_bucket(location)
|
|
51
|
+
if Deploio.resource_exists?("bucket", target_app_name, project)
|
|
52
|
+
return puts "bucket #{target_app_name} already exists, skipping creation"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
system! "nctl create bucket #{target_app_name} -p #{project} --location #{location} " \
|
|
56
|
+
"--permissions \"reader=#{target_app_name};writer=#{target_app_name}\" --wait"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def bucket_env
|
|
60
|
+
schema = env_schema
|
|
61
|
+
{
|
|
62
|
+
schema[:name] => target_app_name,
|
|
63
|
+
schema[:endpoint] => "https://#{bucket_endpoint}",
|
|
64
|
+
schema[:access_key] => bucket_credential("--print-access-key"),
|
|
65
|
+
schema[:secret_key] => bucket_credential("--print-secret-key")
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def bucket_endpoint
|
|
70
|
+
Deploio.fetch_resource("bucket", target_app_name, project).dig("status", "atProvider", "endpoint")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def bucket_credential(flag)
|
|
74
|
+
stdout, = system!("nctl get bucketuser #{target_app_name} -p #{project} #{flag}", quiet: true)
|
|
75
|
+
stdout.strip
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def env_schema
|
|
79
|
+
ENV_SCHEMAS_IN_THE_WILD.find { |schema| app_env?(schema[:secret_key]) } || ENV_SCHEMAS_IN_THE_WILD.first
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Renuo::Cli::Services::Deploio
|
|
4
|
+
class InventorizeOriginResources < Actor # rubocop:disable Metrics/ClassLength
|
|
5
|
+
include ReviewAppHelper
|
|
6
|
+
|
|
7
|
+
input :project
|
|
8
|
+
input :origin_pattern,
|
|
9
|
+
must: {
|
|
10
|
+
be_specific_enough: {
|
|
11
|
+
is: ->(pattern) { pattern.to_s.length >= ReviewAppHelper::MIN_PATTERN_LENGTH },
|
|
12
|
+
message: (lambda do |value:, **|
|
|
13
|
+
"origin pattern '#{value}' must be at least #{ReviewAppHelper::MIN_PATTERN_LENGTH} " \
|
|
14
|
+
"characters long, aborting"
|
|
15
|
+
end)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
output :origin_app_name
|
|
20
|
+
output :origin_database_name
|
|
21
|
+
output :origin_database_location
|
|
22
|
+
output :origin_database_version
|
|
23
|
+
output :origin_database_collation
|
|
24
|
+
output :origin_backup_bucket
|
|
25
|
+
output :origin_backup_path
|
|
26
|
+
output :origin_backup_endpoint
|
|
27
|
+
output :origin_backup_bucket_user
|
|
28
|
+
output :origin_backup_access_key
|
|
29
|
+
output :origin_backup_secret_key
|
|
30
|
+
output :origin_key_value_store_name
|
|
31
|
+
output :origin_key_value_store_location
|
|
32
|
+
output :origin_bucket_name
|
|
33
|
+
output :origin_bucket_endpoint
|
|
34
|
+
output :origin_bucket_user
|
|
35
|
+
output :origin_bucket_access_key
|
|
36
|
+
output :origin_bucket_secret_key
|
|
37
|
+
|
|
38
|
+
def call
|
|
39
|
+
inventorize_app
|
|
40
|
+
inventorize_database
|
|
41
|
+
inventorize_backup
|
|
42
|
+
inventorize_key_value_store
|
|
43
|
+
inventorize_bucket
|
|
44
|
+
ensure_required_tools
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def inventorize_app
|
|
50
|
+
self.origin_app_name = origin_app_name!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def inventorize_database
|
|
54
|
+
database = origin_resource("PostgresDatabase")
|
|
55
|
+
self.origin_database_name = database&.dig("metadata", "name")
|
|
56
|
+
self.origin_database_location, self.origin_database_version, self.origin_database_collation =
|
|
57
|
+
%w[location version collation].map { |key| database&.dig("spec", "forProvider", key)&.to_s }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def inventorize_backup
|
|
61
|
+
self.origin_backup_bucket = backup_bucket_name
|
|
62
|
+
self.origin_backup_path = latest_backup_path
|
|
63
|
+
backup_bucket = bucket_resource(origin_backup_bucket)
|
|
64
|
+
self.origin_backup_endpoint = backup_bucket&.dig("status", "atProvider", "endpoint")
|
|
65
|
+
self.origin_backup_bucket_user = bucket_user(backup_bucket)
|
|
66
|
+
self.origin_backup_access_key, self.origin_backup_secret_key =
|
|
67
|
+
bucketuser_credentials(origin_backup_bucket_user)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def bucket_user(bucket)
|
|
71
|
+
return nil unless bucket
|
|
72
|
+
|
|
73
|
+
bucket_reader_user(bucket) || abort("no bucket user for '#{bucket.dig("metadata", "name")}' in #{project}")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Bucket users are not necessarily named like their bucket; the bucket's permissions reference them.
|
|
77
|
+
def bucket_reader_user(bucket)
|
|
78
|
+
permissions = bucket.dig("spec", "forProvider", "permissions") || []
|
|
79
|
+
permission = permissions.find { |candidate| candidate["role"] == "reader" } || permissions.first
|
|
80
|
+
permission&.dig("userRefs", 0, "name")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Fetched eagerly (even for re-deploys that copy no data) so no origin-side nctl calls remain after inventory.
|
|
84
|
+
def bucketuser_credentials(user)
|
|
85
|
+
return [nil, nil] unless user
|
|
86
|
+
|
|
87
|
+
%w[--print-access-key --print-secret-key].map do |flag|
|
|
88
|
+
stdout, = system!("nctl get bucketuser #{user} -p #{project} #{flag}", quiet: true)
|
|
89
|
+
stdout.strip
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def inventorize_key_value_store
|
|
94
|
+
key_value_store = origin_resource("KeyValueStore")
|
|
95
|
+
self.origin_key_value_store_name = key_value_store&.dig("metadata", "name")
|
|
96
|
+
self.origin_key_value_store_location = key_value_store&.dig("spec", "forProvider", "location")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def inventorize_bucket
|
|
100
|
+
bucket = origin_resource("Bucket")
|
|
101
|
+
self.origin_bucket_name = bucket&.dig("metadata", "name")
|
|
102
|
+
self.origin_bucket_endpoint = bucket&.dig("status", "atProvider", "endpoint")
|
|
103
|
+
self.origin_bucket_user = bucket_user(bucket)
|
|
104
|
+
self.origin_bucket_access_key, self.origin_bucket_secret_key = bucketuser_credentials(origin_bucket_user)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def ensure_required_tools
|
|
108
|
+
missing = required_tools.reject { |tool| installed?(tool) }
|
|
109
|
+
abort "missing #{missing.join(", ")} to copy the origin data, install them and rerun" if missing.any?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def required_tools
|
|
113
|
+
tools = []
|
|
114
|
+
tools += %w[aws pg_restore psql zstd] if origin_backup_path
|
|
115
|
+
tools += %w[aws] if origin_bucket_name
|
|
116
|
+
tools.uniq
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def installed?(tool)
|
|
120
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, tool)) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def resources
|
|
124
|
+
# nctl hides the whole listing behind its no-resources banner when the project holds
|
|
125
|
+
# no nine-owned resources; the plain listing still contains everything else.
|
|
126
|
+
@resources ||= nctl_resources("nctl get all -p #{project} --include-nine-resources -o yaml").presence ||
|
|
127
|
+
nctl_resources("nctl get all -p #{project} -o yaml")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def origin_resources
|
|
131
|
+
@origin_resources ||= resources.select do |resource|
|
|
132
|
+
resource.dig("metadata", "labels", "nine.ch/owned-by") != "nine" &&
|
|
133
|
+
resource.dig("metadata", "name").include?(origin_pattern)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def origin_app_name!
|
|
138
|
+
abort "project '#{project}' holds no resources, is its name right?" if resources.empty?
|
|
139
|
+
|
|
140
|
+
origin_resource("Application")&.dig("metadata", "name") ||
|
|
141
|
+
abort("no origin app (name containing '#{origin_pattern}') found in #{project}")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def origin_resource(kind)
|
|
145
|
+
origin_resources.find { |resource| resource["kind"] == kind }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def resources_of(kind)
|
|
149
|
+
resources.select { |resource| resource["kind"] == kind }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def backup_bucket_name
|
|
153
|
+
return nil unless origin_database_name
|
|
154
|
+
|
|
155
|
+
resources_of("DatabaseBackupSchedule")
|
|
156
|
+
.find { |schedule| schedule.dig("spec", "forProvider", "source", "name") == origin_database_name }
|
|
157
|
+
&.dig("status", "atProvider", "targetBucket", "name")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def latest_backup_path
|
|
161
|
+
return nil unless origin_backup_bucket
|
|
162
|
+
|
|
163
|
+
resources_of("DatabaseBackup")
|
|
164
|
+
.select { |backup| backup.dig("spec", "forProvider", "bucket", "name") == origin_backup_bucket }
|
|
165
|
+
.select { |backup| backup.dig("status", "atProvider", "state") == "succeeded" }
|
|
166
|
+
.max_by { |backup| backup.dig("status", "atProvider", "end").to_s }
|
|
167
|
+
&.dig("status", "atProvider", "path")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def bucket_resource(name)
|
|
171
|
+
name && resources_of("Bucket").find { |bucket| bucket.dig("metadata", "name") == name }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
class Renuo::Cli::Services::Deploio
|
|
6
|
+
class StartTargetApp < Actor
|
|
7
|
+
include ReviewAppHelper
|
|
8
|
+
|
|
9
|
+
input :project
|
|
10
|
+
input :target_app_name, must: satisfy(SafeReviewAppNameContract)
|
|
11
|
+
input :branch
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
puts "starting app #{target_app_name}..."
|
|
15
|
+
system! "nctl update app #{target_app_name} -p #{project} --git-revision #{Shellwords.escape(branch)} " \
|
|
16
|
+
"--skip-repo-access-check --no-pause"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require_relative "create_s3"
|
|
5
|
+
|
|
6
|
+
class Renuo::Cli::Services::Deploio
|
|
7
|
+
class UpdateTargetAppEnv < Actor
|
|
8
|
+
include ReviewAppHelper
|
|
9
|
+
|
|
10
|
+
SENSITIVE_ENV_KEYS = (%w[DATABASE_URL REDIS_URL] + CreateS3::SENSITIVE_ENV_KEYS).freeze
|
|
11
|
+
|
|
12
|
+
input :project
|
|
13
|
+
input :target_app_name, must: satisfy(SafeReviewAppNameContract)
|
|
14
|
+
input :envs
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
return if env.empty?
|
|
18
|
+
|
|
19
|
+
puts "updating env of app #{target_app_name}..."
|
|
20
|
+
system! ["nctl update app #{target_app_name} -p #{project}",
|
|
21
|
+
env_flag("--env", env.except(*SENSITIVE_ENV_KEYS)),
|
|
22
|
+
env_flag("--sensitive-env", env.slice(*SENSITIVE_ENV_KEYS))].compact.join(" ")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def env
|
|
28
|
+
@env ||= envs.reduce({}, :merge)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def env_flag(flag, variables)
|
|
32
|
+
return if variables.empty?
|
|
33
|
+
|
|
34
|
+
"#{flag} #{Shellwords.escape(variables.map { |key, value| "#{key}=#{value}" }.join(";"))}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -3,7 +3,21 @@
|
|
|
3
3
|
require "shellwords"
|
|
4
4
|
|
|
5
5
|
class Renuo::Cli::Services::Deploio
|
|
6
|
+
extend ReviewAppHelper
|
|
7
|
+
|
|
6
8
|
class << self
|
|
9
|
+
# A named nctl get that misses searches every project of the organization,
|
|
10
|
+
# which project-scoped service accounts are forbidden to do.
|
|
11
|
+
def resource_exists?(type, name, project)
|
|
12
|
+
nctl_resources("nctl get #{type} -p #{project} -o yaml").any? do |resource|
|
|
13
|
+
resource.dig("metadata", "name") == name
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fetch_resource(type, name, project)
|
|
18
|
+
nctl_resources("nctl get #{type} #{name} -p #{project} -o yaml").first
|
|
19
|
+
end
|
|
20
|
+
|
|
7
21
|
def fetch_apps
|
|
8
22
|
active_org, available_orgs = fetch_whoami_orgs
|
|
9
23
|
|
|
@@ -33,9 +47,7 @@ class Renuo::Cli::Services::Deploio
|
|
|
33
47
|
end
|
|
34
48
|
|
|
35
49
|
def fetch_vms
|
|
36
|
-
|
|
37
|
-
stdout, stderr, status = Open3.capture3 fetch_vms_cmd
|
|
38
|
-
raise "Error fetching Deploio app list: #{stderr}" unless status.success?
|
|
50
|
+
stdout, = run_nctl_command(%(nctl get cloudvm -A -o yaml))
|
|
39
51
|
|
|
40
52
|
YAML.load_stream(stdout).map do |app|
|
|
41
53
|
{
|
|
@@ -61,7 +73,7 @@ class Renuo::Cli::Services::Deploio
|
|
|
61
73
|
end
|
|
62
74
|
|
|
63
75
|
def run_nctl_command(cmd)
|
|
64
|
-
stdout, stderr, status =
|
|
76
|
+
stdout, stderr, status = system!(cmd, quiet: true, abort_on_failure: false)
|
|
65
77
|
raise "Error fetching Deploio app list: #{stderr}" unless status.success?
|
|
66
78
|
|
|
67
79
|
[stdout, stderr, status]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
version: v1.0
|
|
2
|
+
name: review-app-deploy
|
|
3
|
+
|
|
4
|
+
agent:
|
|
5
|
+
machine:
|
|
6
|
+
type: e1-standard-2
|
|
7
|
+
os_image: ubuntu2404
|
|
8
|
+
|
|
9
|
+
blocks:
|
|
10
|
+
- name: review-app-deploy
|
|
11
|
+
execution_time_limit:
|
|
12
|
+
minutes: 30
|
|
13
|
+
task:
|
|
14
|
+
secrets:
|
|
15
|
+
- name: <%= secret_name %>
|
|
16
|
+
jobs:
|
|
17
|
+
- name: deploying
|
|
18
|
+
commands:
|
|
19
|
+
- gem i -q renuo-cli
|
|
20
|
+
- renuo ci setup
|
|
21
|
+
# pg_restore rejects backups dumped by a newer PostgreSQL, so match or exceed the origin database's version.
|
|
22
|
+
- sudo apt-get install -qq postgresql-common
|
|
23
|
+
- sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
|
|
24
|
+
- sudo apt-get install -qq postgresql-client-18
|
|
25
|
+
- export PATH=/usr/lib/postgresql/18/bin:$PATH
|
|
26
|
+
- renuo review-app:deploy --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH --origin-pattern <%= origin_pattern %>
|
|
27
|
+
# Make sure your review apps have the right environment variables. We don't copy from origin.
|
|
28
|
+
#
|
|
29
|
+
# -| nctl update app $(renuo review-app:name --branch $SEMAPHORE_GIT_WORKING_BRANCH) --project <%= project %> \
|
|
30
|
+
# --env "KEY=value" --sensitive-env "SECRET=$FROM_SEMAPHORE_SECRET" \
|
|
31
|
+
# --build-env "KEY=value" --sensitive-build-env "KEY=value"
|
|
32
|
+
#
|
|
33
|
+
# To find out which env variables a previously existing app uses, inspect its spec:
|
|
34
|
+
#
|
|
35
|
+
# - nctl get app existing-app --project <%= project %> -o yaml
|
|
36
|
+
#
|
|
37
|
+
# The runtime env is at .spec.forProvider.config.env, the build env at .spec.forProvider.buildEnv.
|
|
38
|
+
#
|
|
39
|
+
# If the origin's env variables are safe to reuse on review apps, you can replace this whole
|
|
40
|
+
# block (deploy, env setup, and start) with a single command:
|
|
41
|
+
#
|
|
42
|
+
# - renuo review-app:deploy --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH --origin-pattern <%= origin_pattern %> --keep-envs=build,sensitive-build,runtime,sensitive-runtime --start
|
|
43
|
+
- renuo review-app:start --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH
|
|
44
|
+
- renuo ci check-deploio-status $(renuo review-app:name --branch $SEMAPHORE_GIT_WORKING_BRANCH) --project <%= project %>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
version: v1.0
|
|
2
|
+
name: review-app-purge
|
|
3
|
+
|
|
4
|
+
agent:
|
|
5
|
+
machine:
|
|
6
|
+
type: e1-standard-2
|
|
7
|
+
os_image: ubuntu2404
|
|
8
|
+
|
|
9
|
+
blocks:
|
|
10
|
+
- name: review-app-purge
|
|
11
|
+
execution_time_limit:
|
|
12
|
+
minutes: 15
|
|
13
|
+
task:
|
|
14
|
+
secrets:
|
|
15
|
+
- name: <%= secret_name %>
|
|
16
|
+
jobs:
|
|
17
|
+
- name: purging
|
|
18
|
+
commands:
|
|
19
|
+
- gem i -q renuo-cli
|
|
20
|
+
- renuo ci setup
|
|
21
|
+
- renuo review-app:purge --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH
|
data/lib/renuo/cli/version.rb
CHANGED
data/lib/renuo/cli.rb
CHANGED
|
@@ -10,11 +10,21 @@ require "net/http"
|
|
|
10
10
|
require "open3"
|
|
11
11
|
require "psych"
|
|
12
12
|
require "securerandom"
|
|
13
|
+
require "service_actor"
|
|
13
14
|
require "tmpdir"
|
|
14
15
|
require "uri"
|
|
15
16
|
require "renuo/cli/version"
|
|
16
17
|
|
|
18
|
+
module Renuo
|
|
19
|
+
class Cli
|
|
20
|
+
module Commands; end
|
|
21
|
+
module Services; end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
17
25
|
Dir[File.expand_path("cli/helpers/*.rb", __dir__)].each { |f| require f }
|
|
26
|
+
require_relative "cli/services/deploio"
|
|
27
|
+
Dir[File.expand_path("cli/services/deploio/*.rb", __dir__)].each { |f| require f }
|
|
18
28
|
|
|
19
29
|
def agree(question)
|
|
20
30
|
question += " [y/n]" if question
|