cpflow 3.0.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 +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
|