cpflow 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/check_cpln_links.yml +19 -0
- data/.github/workflows/command_docs.yml +24 -0
- data/.github/workflows/rspec-shared.yml +56 -0
- data/.github/workflows/rspec.yml +28 -0
- data/.github/workflows/rubocop.yml +24 -0
- data/.gitignore +18 -0
- data/.overcommit.yml +16 -0
- data/.rubocop.yml +22 -0
- data/.simplecov_spawn.rb +10 -0
- data/CHANGELOG.md +259 -0
- data/CONTRIBUTING.md +73 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +126 -0
- data/LICENSE +21 -0
- data/README.md +546 -0
- data/Rakefile +21 -0
- data/bin/cpflow +6 -0
- data/cpflow +6 -0
- data/cpflow.gemspec +41 -0
- data/docs/assets/grafana-alert.png +0 -0
- data/docs/assets/memcached.png +0 -0
- data/docs/assets/sidekiq-pre-stop-hook.png +0 -0
- data/docs/commands.md +454 -0
- data/docs/dns.md +15 -0
- data/docs/migrating.md +262 -0
- data/docs/postgres.md +436 -0
- data/docs/redis.md +128 -0
- data/docs/secrets-and-env-values.md +42 -0
- data/docs/tips.md +150 -0
- data/docs/troubleshooting.md +6 -0
- data/examples/circleci.yml +104 -0
- data/examples/controlplane.yml +159 -0
- data/lib/command/apply_template.rb +209 -0
- data/lib/command/base.rb +540 -0
- data/lib/command/build_image.rb +49 -0
- data/lib/command/cleanup_images.rb +136 -0
- data/lib/command/cleanup_stale_apps.rb +79 -0
- data/lib/command/config.rb +48 -0
- data/lib/command/copy_image_from_upstream.rb +108 -0
- data/lib/command/delete.rb +149 -0
- data/lib/command/deploy_image.rb +56 -0
- data/lib/command/doctor.rb +47 -0
- data/lib/command/env.rb +22 -0
- data/lib/command/exists.rb +23 -0
- data/lib/command/generate.rb +45 -0
- data/lib/command/info.rb +222 -0
- data/lib/command/latest_image.rb +19 -0
- data/lib/command/logs.rb +49 -0
- data/lib/command/maintenance.rb +42 -0
- data/lib/command/maintenance_off.rb +62 -0
- data/lib/command/maintenance_on.rb +62 -0
- data/lib/command/maintenance_set_page.rb +34 -0
- data/lib/command/no_command.rb +23 -0
- data/lib/command/open.rb +33 -0
- data/lib/command/open_console.rb +26 -0
- data/lib/command/promote_app_from_upstream.rb +38 -0
- data/lib/command/ps.rb +41 -0
- data/lib/command/ps_restart.rb +37 -0
- data/lib/command/ps_start.rb +51 -0
- data/lib/command/ps_stop.rb +82 -0
- data/lib/command/ps_wait.rb +40 -0
- data/lib/command/run.rb +573 -0
- data/lib/command/setup_app.rb +113 -0
- data/lib/command/test.rb +23 -0
- data/lib/command/version.rb +18 -0
- data/lib/constants/exit_code.rb +7 -0
- data/lib/core/config.rb +316 -0
- data/lib/core/controlplane.rb +552 -0
- data/lib/core/controlplane_api.rb +170 -0
- data/lib/core/controlplane_api_direct.rb +112 -0
- data/lib/core/doctor_service.rb +104 -0
- data/lib/core/helpers.rb +26 -0
- data/lib/core/shell.rb +100 -0
- data/lib/core/template_parser.rb +76 -0
- data/lib/cpflow/version.rb +6 -0
- data/lib/cpflow.rb +288 -0
- data/lib/deprecated_commands.json +9 -0
- data/lib/generator_templates/Dockerfile +27 -0
- data/lib/generator_templates/controlplane.yml +62 -0
- data/lib/generator_templates/entrypoint.sh +8 -0
- data/lib/generator_templates/templates/app.yml +21 -0
- data/lib/generator_templates/templates/postgres.yml +176 -0
- data/lib/generator_templates/templates/rails.yml +36 -0
- data/rakelib/create_release.rake +81 -0
- data/script/add_command +37 -0
- data/script/check_command_docs +3 -0
- data/script/check_cpln_links +45 -0
- data/script/rename_command +43 -0
- data/script/update_command_docs +62 -0
- data/templates/app.yml +13 -0
- data/templates/daily-task.yml +32 -0
- data/templates/maintenance.yml +25 -0
- data/templates/memcached.yml +24 -0
- data/templates/postgres.yml +32 -0
- data/templates/rails.yml +27 -0
- data/templates/redis.yml +21 -0
- data/templates/redis2.yml +37 -0
- data/templates/sidekiq.yml +38 -0
- metadata +341 -0
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ControlplaneApi # rubocop:disable Metrics/ClassLength
|
4
|
+
def list_orgs
|
5
|
+
api_json("/org", method: :get)
|
6
|
+
end
|
7
|
+
|
8
|
+
def gvc_list(org:)
|
9
|
+
api_json("/org/#{org}/gvc", method: :get)
|
10
|
+
end
|
11
|
+
|
12
|
+
def gvc_get(org:, gvc:)
|
13
|
+
api_json("/org/#{org}/gvc/#{gvc}", method: :get)
|
14
|
+
end
|
15
|
+
|
16
|
+
def gvc_delete(org:, gvc:)
|
17
|
+
api_json("/org/#{org}/gvc/#{gvc}", method: :delete)
|
18
|
+
end
|
19
|
+
|
20
|
+
def query_images(org:, gvc:, gvc_op_type:)
|
21
|
+
terms = [
|
22
|
+
{
|
23
|
+
property: "repository",
|
24
|
+
op: gvc_op_type,
|
25
|
+
value: gvc
|
26
|
+
}
|
27
|
+
]
|
28
|
+
|
29
|
+
query("/org/#{org}/image", terms)
|
30
|
+
end
|
31
|
+
|
32
|
+
def fetch_image_details(org:, image:)
|
33
|
+
api_json("/org/#{org}/image/#{image}", method: :get)
|
34
|
+
end
|
35
|
+
|
36
|
+
def image_delete(org:, image:)
|
37
|
+
api_json("/org/#{org}/image/#{image}", method: :delete)
|
38
|
+
end
|
39
|
+
|
40
|
+
def log_get(org:, gvc:, workload: nil, replica: nil, from: nil, to: nil) # rubocop:disable Metrics/ParameterLists
|
41
|
+
query = { gvc: gvc }
|
42
|
+
query[:workload] = workload if workload
|
43
|
+
query[:replica] = replica if replica
|
44
|
+
query = query.map { |k, v| %(#{k}="#{v}") }.join(",").then { "{#{_1}}" }
|
45
|
+
|
46
|
+
params = { query: query }
|
47
|
+
params[:from] = "#{from}000000000" if from
|
48
|
+
params[:to] = "#{to}000000000" if to
|
49
|
+
params[:limit] = "5000"
|
50
|
+
# params << "delay_for=0"
|
51
|
+
# params << "limit=30"
|
52
|
+
# params << "direction=forward"
|
53
|
+
params = params.map { |k, v| %(#{k}=#{CGI.escape(v)}) }.join("&")
|
54
|
+
|
55
|
+
api_json("/logs/org/#{org}/loki/api/v1/query_range?#{params}", method: :get, host: :logs)
|
56
|
+
end
|
57
|
+
|
58
|
+
def query_workloads(org:, gvc:, workload:, gvc_op_type:, workload_op_type:) # rubocop:disable Metrics/MethodLength
|
59
|
+
terms = [
|
60
|
+
{
|
61
|
+
rel: "gvc",
|
62
|
+
op: gvc_op_type,
|
63
|
+
value: gvc
|
64
|
+
},
|
65
|
+
{
|
66
|
+
property: "name",
|
67
|
+
op: workload_op_type,
|
68
|
+
value: workload
|
69
|
+
}
|
70
|
+
]
|
71
|
+
|
72
|
+
query("/org/#{org}/workload", terms)
|
73
|
+
end
|
74
|
+
|
75
|
+
def workload_list(org:, gvc:)
|
76
|
+
api_json("/org/#{org}/gvc/#{gvc}/workload", method: :get)
|
77
|
+
end
|
78
|
+
|
79
|
+
def workload_list_by_org(org:)
|
80
|
+
api_json("/org/#{org}/workload", method: :get)
|
81
|
+
end
|
82
|
+
|
83
|
+
def workload_get(org:, gvc:, workload:)
|
84
|
+
api_json("/org/#{org}/gvc/#{gvc}/workload/#{workload}", method: :get)
|
85
|
+
end
|
86
|
+
|
87
|
+
def update_workload(org:, gvc:, workload:, data:)
|
88
|
+
api_json("/org/#{org}/gvc/#{gvc}/workload/#{workload}", method: :patch, body: data)
|
89
|
+
end
|
90
|
+
|
91
|
+
def workload_deployments(org:, gvc:, workload:)
|
92
|
+
api_json("/org/#{org}/gvc/#{gvc}/workload/#{workload}/deployment", method: :get)
|
93
|
+
end
|
94
|
+
|
95
|
+
def delete_workload(org:, gvc:, workload:)
|
96
|
+
api_json("/org/#{org}/gvc/#{gvc}/workload/#{workload}", method: :delete)
|
97
|
+
end
|
98
|
+
|
99
|
+
def list_volumesets(org:, gvc:)
|
100
|
+
api_json("/org/#{org}/gvc/#{gvc}/volumeset", method: :get)
|
101
|
+
end
|
102
|
+
|
103
|
+
def delete_volumeset(org:, gvc:, volumeset:)
|
104
|
+
api_json("/org/#{org}/gvc/#{gvc}/volumeset/#{volumeset}", method: :delete)
|
105
|
+
end
|
106
|
+
|
107
|
+
def fetch_domain(org:, domain:)
|
108
|
+
api_json("/org/#{org}/domain/#{domain}", method: :get)
|
109
|
+
end
|
110
|
+
|
111
|
+
def list_domains(org:)
|
112
|
+
api_json("/org/#{org}/domain", method: :get)
|
113
|
+
end
|
114
|
+
|
115
|
+
def update_domain(org:, domain:, data:)
|
116
|
+
api_json("/org/#{org}/domain/#{domain}", method: :patch, body: data)
|
117
|
+
end
|
118
|
+
|
119
|
+
def fetch_secret(org:, secret:)
|
120
|
+
api_json("/org/#{org}/secret/#{secret}", method: :get)
|
121
|
+
end
|
122
|
+
|
123
|
+
def delete_secret(org:, secret:)
|
124
|
+
api_json("/org/#{org}/secret/#{secret}", method: :delete)
|
125
|
+
end
|
126
|
+
|
127
|
+
def fetch_identity(org:, gvc:, identity:)
|
128
|
+
api_json("/org/#{org}/gvc/#{gvc}/identity/#{identity}", method: :get)
|
129
|
+
end
|
130
|
+
|
131
|
+
def fetch_policy(org:, policy:)
|
132
|
+
api_json("/org/#{org}/policy/#{policy}", method: :get)
|
133
|
+
end
|
134
|
+
|
135
|
+
def delete_policy(org:, policy:)
|
136
|
+
api_json("/org/#{org}/policy/#{policy}", method: :delete)
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def fetch_query_pages(result)
|
142
|
+
loop do
|
143
|
+
next_page_url = result["links"].find { |link| link["rel"] == "next" }&.dig("href")
|
144
|
+
break unless next_page_url
|
145
|
+
|
146
|
+
next_page_result = api_json(next_page_url, method: :get)
|
147
|
+
result["items"] += next_page_result["items"]
|
148
|
+
result["links"] = next_page_result["links"]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def query(url, terms)
|
153
|
+
body = {
|
154
|
+
kind: "string",
|
155
|
+
spec: {
|
156
|
+
match: "all",
|
157
|
+
terms: terms
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
result = api_json("#{url}/-query", method: :post, body: body)
|
162
|
+
fetch_query_pages(result)
|
163
|
+
|
164
|
+
result
|
165
|
+
end
|
166
|
+
|
167
|
+
def api_json(...)
|
168
|
+
ControlplaneApiDirect.new.call(...)
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ControlplaneApiDirect
|
4
|
+
API_METHODS = {
|
5
|
+
get: Net::HTTP::Get,
|
6
|
+
patch: Net::HTTP::Patch,
|
7
|
+
post: Net::HTTP::Post,
|
8
|
+
put: Net::HTTP::Put,
|
9
|
+
delete: Net::HTTP::Delete
|
10
|
+
}.freeze
|
11
|
+
API_HOSTS = { api: "https://api.cpln.io", logs: "https://logs.cpln.io" }.freeze
|
12
|
+
|
13
|
+
# API_TOKEN_REGEX = Regexp.union(
|
14
|
+
# /^[\w.]{155}$/, # CPLN_TOKEN format
|
15
|
+
# /^[\w\-._]{1134}$/ # 'cpln profile token' format
|
16
|
+
# ).freeze
|
17
|
+
|
18
|
+
API_TOKEN_REGEX = /^[\w\-._]+$/.freeze
|
19
|
+
API_TOKEN_EXPIRY_SECONDS = 300
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_accessor :trace
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(url, method:, host: :api, body: nil) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
26
|
+
trace = ControlplaneApiDirect.trace
|
27
|
+
uri = URI("#{api_host(host)}#{url}")
|
28
|
+
request = API_METHODS[method].new(uri)
|
29
|
+
request["Content-Type"] = "application/json"
|
30
|
+
|
31
|
+
refresh_api_token if should_refresh_api_token?
|
32
|
+
|
33
|
+
request["Authorization"] = api_token[:token]
|
34
|
+
request.body = body.to_json if body
|
35
|
+
|
36
|
+
Shell.debug(method.upcase, "#{uri} #{body&.to_json}")
|
37
|
+
|
38
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
39
|
+
http.use_ssl = uri.scheme == "https"
|
40
|
+
http.set_debug_output($stdout) if trace
|
41
|
+
|
42
|
+
response = http.start { |ht| ht.request(request) }
|
43
|
+
|
44
|
+
case response
|
45
|
+
when Net::HTTPOK
|
46
|
+
JSON.parse(response.body)
|
47
|
+
when Net::HTTPAccepted
|
48
|
+
true
|
49
|
+
when Net::HTTPNotFound
|
50
|
+
nil
|
51
|
+
when Net::HTTPForbidden
|
52
|
+
org = self.class.parse_org(url)
|
53
|
+
raise("Double check your org #{org}. #{response} #{response.body}")
|
54
|
+
else
|
55
|
+
raise("#{response} #{response.body}")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def api_host(host)
|
60
|
+
case host
|
61
|
+
when :api
|
62
|
+
ENV.fetch("CPLN_ENDPOINT", API_HOSTS[host])
|
63
|
+
else
|
64
|
+
API_HOSTS[host]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# rubocop:disable Style/ClassVars
|
69
|
+
def api_token # rubocop:disable Metrics/MethodLength
|
70
|
+
return @@api_token if defined?(@@api_token)
|
71
|
+
|
72
|
+
@@api_token = {
|
73
|
+
token: ENV.fetch("CPLN_TOKEN", nil),
|
74
|
+
comes_from_profile: false
|
75
|
+
}
|
76
|
+
if @@api_token[:token].nil?
|
77
|
+
@@api_token = {
|
78
|
+
token: Shell.cmd("cpln", "profile", "token")[:output].chomp,
|
79
|
+
comes_from_profile: true
|
80
|
+
}
|
81
|
+
end
|
82
|
+
return @@api_token if @@api_token[:token].match?(API_TOKEN_REGEX)
|
83
|
+
|
84
|
+
raise "Unknown API token format. " \
|
85
|
+
"Please re-run 'cpln profile login' or set the correct CPLN_TOKEN env variable."
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns `true` when the token is about to expire in 5 minutes
|
89
|
+
def should_refresh_api_token?
|
90
|
+
return false unless api_token[:comes_from_profile]
|
91
|
+
|
92
|
+
payload, = JWT.decode(api_token[:token], nil, false)
|
93
|
+
difference_in_seconds = payload["exp"] - Time.now.to_i
|
94
|
+
|
95
|
+
difference_in_seconds <= API_TOKEN_EXPIRY_SECONDS
|
96
|
+
rescue JWT::DecodeError
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
100
|
+
def refresh_api_token
|
101
|
+
@@api_token[:token] = Shell.cmd("cpln", "profile", "token")[:output].chomp
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.reset_api_token
|
105
|
+
remove_class_variable(:@@api_token) if defined?(@@api_token)
|
106
|
+
end
|
107
|
+
# rubocop:enable Style/ClassVars
|
108
|
+
|
109
|
+
def self.parse_org(url)
|
110
|
+
url.match(%r{^/org/([^/]+)})[1]
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ValidationError < StandardError; end
|
4
|
+
|
5
|
+
class DoctorService
|
6
|
+
attr_reader :config
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
def run_validations(validations, silent_if_passing: false) # rubocop:disable Metrics/MethodLength
|
13
|
+
@any_failed_validation = false
|
14
|
+
|
15
|
+
validations.each do |validation|
|
16
|
+
case validation
|
17
|
+
when "config"
|
18
|
+
validate_config
|
19
|
+
when "templates"
|
20
|
+
validate_templates
|
21
|
+
else
|
22
|
+
raise ValidationError, Shell.color("ERROR: Invalid validation '#{validation}'.", :red)
|
23
|
+
end
|
24
|
+
|
25
|
+
progress.puts("#{Shell.color('[PASS]', :green)} #{validation}") unless silent_if_passing
|
26
|
+
rescue ValidationError => e
|
27
|
+
@any_failed_validation = true
|
28
|
+
|
29
|
+
progress.puts("#{Shell.color('[FAIL]', :red)} #{validation}\n\n#{e.message}\n\n")
|
30
|
+
end
|
31
|
+
|
32
|
+
exit(ExitCode::ERROR_DEFAULT) if @any_failed_validation
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_config
|
36
|
+
check_for_app_names_contained_in_others
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_templates
|
40
|
+
@template_parser = TemplateParser.new(config)
|
41
|
+
filenames = Dir.glob("#{@template_parser.template_dir}/*.yml")
|
42
|
+
templates = @template_parser.parse(filenames)
|
43
|
+
|
44
|
+
check_for_duplicate_templates(templates)
|
45
|
+
warn_deprecated_template_variables
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def check_for_app_names_contained_in_others
|
51
|
+
app_names_contained_in_others = find_app_names_contained_in_others
|
52
|
+
return if app_names_contained_in_others.empty?
|
53
|
+
|
54
|
+
message = "App names contained in others found below. Please ensure that app names are unique."
|
55
|
+
list = app_names_contained_in_others
|
56
|
+
.map { |app_prefix, app_name| " - '#{app_prefix}' is a prefix of '#{app_name}'" }
|
57
|
+
.join("\n")
|
58
|
+
raise ValidationError, "#{Shell.color("ERROR: #{message}", :red)}\n#{list}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def find_app_names_contained_in_others # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
62
|
+
app_names = config.apps.keys.map(&:to_s).sort
|
63
|
+
app_prefixes = config.apps
|
64
|
+
.select { |_, app_options| app_options[:match_if_app_name_starts_with] }
|
65
|
+
.keys
|
66
|
+
.map(&:to_s)
|
67
|
+
.sort
|
68
|
+
app_prefixes.each_with_object([]) do |app_prefix, app_names_contained_in_others|
|
69
|
+
app_names.each do |app_name|
|
70
|
+
if app_prefix != app_name && app_name.start_with?(app_prefix)
|
71
|
+
app_names_contained_in_others.push([app_prefix, app_name])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def check_for_duplicate_templates(templates)
|
78
|
+
grouped_templates = templates.group_by { |template| [template["kind"], template["name"]] }
|
79
|
+
duplicate_templates = grouped_templates.select { |_, group| group.size > 1 }
|
80
|
+
return if duplicate_templates.empty?
|
81
|
+
|
82
|
+
message = "Duplicate templates found with the kind/names below. Please ensure that templates are unique."
|
83
|
+
list = duplicate_templates
|
84
|
+
.map { |(kind, name), _| " - kind: #{kind}, name: #{name}" }
|
85
|
+
.join("\n")
|
86
|
+
raise ValidationError, "#{Shell.color("ERROR: #{message}", :red)}\n#{list}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def warn_deprecated_template_variables
|
90
|
+
deprecated_variables = @template_parser.deprecated_variables
|
91
|
+
return if deprecated_variables.empty?
|
92
|
+
|
93
|
+
message = "Please replace these variables in the templates, " \
|
94
|
+
"as support for them will be removed in a future major version bump:"
|
95
|
+
list = deprecated_variables
|
96
|
+
.map { |old_key, new_key| " - #{old_key} -> #{new_key}" }
|
97
|
+
.join("\n")
|
98
|
+
progress.puts("\n#{Shell.color("DEPRECATED: #{message}", :yellow)}\n#{list}\n\n")
|
99
|
+
end
|
100
|
+
|
101
|
+
def progress
|
102
|
+
$stderr
|
103
|
+
end
|
104
|
+
end
|
data/lib/core/helpers.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module Helpers
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def strip_str_and_validate(str)
|
9
|
+
return str if str.nil?
|
10
|
+
|
11
|
+
str = str.strip
|
12
|
+
str.empty? ? nil : str
|
13
|
+
end
|
14
|
+
|
15
|
+
def random_four_digits
|
16
|
+
SecureRandom.random_number(1000..9999)
|
17
|
+
end
|
18
|
+
|
19
|
+
def normalize_command_name(name)
|
20
|
+
name.to_s.tr("_", "-")
|
21
|
+
end
|
22
|
+
|
23
|
+
def normalize_option_name(name)
|
24
|
+
"--#{name.to_s.tr('_', '-')}"
|
25
|
+
end
|
26
|
+
end
|
data/lib/core/shell.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shell
|
4
|
+
class << self
|
5
|
+
attr_reader :tmp_stderr, :verbose
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.shell
|
9
|
+
@shell ||= Thor::Shell::Color.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.use_tmp_stderr
|
13
|
+
@tmp_stderr = Tempfile.create
|
14
|
+
|
15
|
+
yield
|
16
|
+
|
17
|
+
@tmp_stderr.close
|
18
|
+
@tmp_stderr = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.write_to_tmp_stderr(message)
|
22
|
+
tmp_stderr.write(message)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.read_from_tmp_stderr
|
26
|
+
tmp_stderr.rewind
|
27
|
+
tmp_stderr.read.strip
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.color(message, color_key)
|
31
|
+
shell.set_color(message, color_key)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.confirm(message)
|
35
|
+
shell.yes?("#{message} (y/N)")
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.warn(message)
|
39
|
+
Kernel.warn(color("WARNING: #{message}", :yellow))
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.warn_deprecated(message)
|
43
|
+
Kernel.warn(color("DEPRECATED: #{message}", :yellow))
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.abort(message, exit_status = ExitCode::ERROR_DEFAULT)
|
47
|
+
Kernel.warn(color("ERROR: #{message}", :red))
|
48
|
+
exit(exit_status)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.verbose_mode(verbose)
|
52
|
+
@verbose = verbose
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.debug(prefix, message, sensitive_data_pattern: nil)
|
56
|
+
return unless verbose
|
57
|
+
|
58
|
+
filtered_message = hide_sensitive_data(message, sensitive_data_pattern)
|
59
|
+
Kernel.warn("\n[#{color(prefix, :red)}] #{filtered_message}")
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.should_hide_output?
|
63
|
+
tmp_stderr && !verbose
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.cmd(*cmd_to_run, capture_stderr: false)
|
67
|
+
output, status = capture_stderr ? Open3.capture2e(*cmd_to_run) : Open3.capture2(*cmd_to_run)
|
68
|
+
|
69
|
+
{
|
70
|
+
output: output,
|
71
|
+
success: status.success?
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# Hide sensitive data based on the passed pattern
|
77
|
+
#
|
78
|
+
# @param [String] message
|
79
|
+
# The message to get processed.
|
80
|
+
# @param [Regexp, nil] pattern
|
81
|
+
# The regular expression to be used. If not provided, no filter gets applied.
|
82
|
+
#
|
83
|
+
# @return [String]
|
84
|
+
# Filtered message.
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# hide_sensitive_data("--token abcd", /(?<=--token )(\S+)/)
|
88
|
+
def self.hide_sensitive_data(message, pattern = nil)
|
89
|
+
return message unless pattern.is_a?(Regexp)
|
90
|
+
|
91
|
+
message.gsub(pattern, "XXXXXXX")
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.trap_interrupt
|
95
|
+
trap("SIGINT") do
|
96
|
+
puts
|
97
|
+
exit(ExitCode::INTERRUPT)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TemplateParser
|
4
|
+
attr_reader :config, :deprecated_variables
|
5
|
+
|
6
|
+
def initialize(config)
|
7
|
+
@config = config
|
8
|
+
end
|
9
|
+
|
10
|
+
def template_dir
|
11
|
+
"#{config.app_cpln_dir}/templates"
|
12
|
+
end
|
13
|
+
|
14
|
+
def template_filename(name)
|
15
|
+
"#{template_dir}/#{name}.yml"
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse(filenames)
|
19
|
+
@deprecated_variables = {}
|
20
|
+
|
21
|
+
filenames.each_with_object([]) do |filename, templates|
|
22
|
+
yaml_file = File.read(filename)
|
23
|
+
yaml_file = replace_variables(yaml_file)
|
24
|
+
|
25
|
+
template_yamls = yaml_file.split(/^---\s*$/)
|
26
|
+
template_yamls.each do |template_yaml|
|
27
|
+
template = YAML.safe_load(template_yaml)
|
28
|
+
templates.push(template)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def replace_variables(yaml_file) # rubocop:disable Metrics/MethodLength
|
36
|
+
yaml_file = yaml_file
|
37
|
+
.gsub("{{APP_ORG}}", config.org)
|
38
|
+
.gsub("{{APP_NAME}}", config.app)
|
39
|
+
.gsub("{{APP_LOCATION}}", config.location)
|
40
|
+
.gsub("{{APP_LOCATION_LINK}}", config.location_link)
|
41
|
+
.gsub("{{APP_IMAGE}}", cp.latest_image)
|
42
|
+
.gsub("{{APP_IMAGE_LINK}}", config.image_link(cp.latest_image))
|
43
|
+
.gsub("{{APP_IDENTITY}}", config.identity)
|
44
|
+
.gsub("{{APP_IDENTITY_LINK}}", config.identity_link)
|
45
|
+
.gsub("{{APP_SECRETS}}", config.secrets)
|
46
|
+
.gsub("{{APP_SECRETS_POLICY}}", config.secrets_policy)
|
47
|
+
|
48
|
+
find_deprecated_variables(yaml_file)
|
49
|
+
|
50
|
+
# Kept for backwards compatibility
|
51
|
+
yaml_file
|
52
|
+
.gsub("APP_ORG", config.org)
|
53
|
+
.gsub("APP_GVC", config.app)
|
54
|
+
.gsub("APP_LOCATION", config.location)
|
55
|
+
.gsub("APP_IMAGE", cp.latest_image)
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_deprecated_variables(yaml_file)
|
59
|
+
new_variables.each do |old_key, new_key|
|
60
|
+
@deprecated_variables[old_key] = new_key if yaml_file.include?(old_key)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def new_variables
|
65
|
+
{
|
66
|
+
"APP_ORG" => "{{APP_ORG}}",
|
67
|
+
"APP_GVC" => "{{APP_NAME}}",
|
68
|
+
"APP_LOCATION" => "{{APP_LOCATION}}",
|
69
|
+
"APP_IMAGE" => "{{APP_IMAGE}}"
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def cp
|
74
|
+
@cp ||= Controlplane.new(config)
|
75
|
+
end
|
76
|
+
end
|