cpflow 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/claude-code-review.yml +44 -0
- data/.github/workflows/claude.yml +50 -0
- data/.gitignore +6 -0
- data/CHANGELOG.md +17 -1
- data/Gemfile.lock +2 -2
- data/README.md +17 -14
- data/docs/ci-automation.md +28 -0
- data/docs/commands.md +21 -1
- data/docs/terraform/details.md +415 -0
- data/docs/terraform/example/.controlplane/controlplane.yml +29 -0
- data/docs/terraform/example/.controlplane/templates/app.yml +38 -0
- data/docs/terraform/example/.controlplane/templates/postgres.yml +30 -0
- data/docs/terraform/example/.controlplane/templates/rails.yml +26 -0
- data/docs/terraform/overview.md +105 -0
- data/lib/command/base.rb +29 -5
- data/lib/command/base_sub_command.rb +15 -0
- data/lib/command/generate.rb +1 -1
- data/lib/command/ps.rb +1 -1
- data/lib/command/ps_stop.rb +2 -1
- data/lib/command/ps_wait.rb +5 -1
- data/lib/command/run.rb +4 -21
- data/lib/command/terraform/base.rb +35 -0
- data/lib/command/terraform/generate.rb +99 -0
- data/lib/command/terraform/import.rb +79 -0
- data/lib/core/config.rb +1 -1
- data/lib/core/controlplane.rb +7 -6
- data/lib/core/controlplane_api_direct.rb +23 -1
- data/lib/core/shell.rb +9 -4
- data/lib/core/terraform_config/agent.rb +31 -0
- data/lib/core/terraform_config/audit_context.rb +31 -0
- data/lib/core/terraform_config/base.rb +25 -0
- data/lib/core/terraform_config/dsl.rb +102 -0
- data/lib/core/terraform_config/generator.rb +184 -0
- data/lib/core/terraform_config/gvc.rb +63 -0
- data/lib/core/terraform_config/identity.rb +35 -0
- data/lib/core/terraform_config/local_variable.rb +30 -0
- data/lib/core/terraform_config/policy.rb +151 -0
- data/lib/core/terraform_config/provider.rb +22 -0
- data/lib/core/terraform_config/required_provider.rb +23 -0
- data/lib/core/terraform_config/secret.rb +138 -0
- data/lib/core/terraform_config/volume_set.rb +155 -0
- data/lib/core/terraform_config/workload/main.tf +316 -0
- data/lib/core/terraform_config/workload/required_providers.tf +8 -0
- data/lib/core/terraform_config/workload/variables.tf +263 -0
- data/lib/core/terraform_config/workload.rb +132 -0
- data/lib/cpflow/version.rb +1 -1
- data/lib/cpflow.rb +51 -9
- data/lib/generator_templates/templates/postgres.yml +1 -1
- data/lib/patches/array.rb +8 -0
- data/lib/patches/hash.rb +47 -0
- data/lib/patches/string.rb +34 -0
- data/script/update_command_docs +6 -2
- metadata +37 -4
- /data/docs/{migrating.md → migrating-heroku-to-control-plane.md} +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Inspired by https://github.com/rails/thor/wiki/Subcommands
|
|
4
|
+
class BaseSubCommand < Thor
|
|
5
|
+
def self.banner(command, _namespace = nil, _subcommand = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
6
|
+
"#{basename} #{subcommand_prefix} #{command.usage}"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.subcommand_prefix
|
|
10
|
+
name
|
|
11
|
+
.gsub(/.*::/, "")
|
|
12
|
+
.gsub(/^[A-Z]/) { |match| match[0].downcase }
|
|
13
|
+
.gsub(/[A-Z]/) { |match| "-#{match[0].downcase}" }
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/command/generate.rb
CHANGED
data/lib/command/ps.rb
CHANGED
data/lib/command/ps_stop.rb
CHANGED
|
@@ -75,7 +75,8 @@ module Command
|
|
|
75
75
|
|
|
76
76
|
step("Waiting for replica '#{replica}' to not be ready", retry_on_failure: true) do
|
|
77
77
|
result = cp.fetch_workload_replicas(workload, location: config.location)
|
|
78
|
-
|
|
78
|
+
items = result&.dig("items")
|
|
79
|
+
items && !items.include?(replica)
|
|
79
80
|
end
|
|
80
81
|
end
|
|
81
82
|
end
|
data/lib/command/ps_wait.rb
CHANGED
|
@@ -11,6 +11,7 @@ module Command
|
|
|
11
11
|
DESCRIPTION = "Waits for workloads in app to be ready after re-deployment"
|
|
12
12
|
LONG_DESCRIPTION = <<~DESC
|
|
13
13
|
- Waits for workloads in app to be ready after re-deployment
|
|
14
|
+
- Use Unix timeout command to set a maximum wait time (e.g., `timeout 300 cpflow ps:wait ...`)
|
|
14
15
|
DESC
|
|
15
16
|
EXAMPLES = <<~EX
|
|
16
17
|
```sh
|
|
@@ -18,7 +19,10 @@ module Command
|
|
|
18
19
|
cpflow ps:wait -a $APP_NAME
|
|
19
20
|
|
|
20
21
|
# Waits for a specific workload in app.
|
|
21
|
-
cpflow ps:
|
|
22
|
+
cpflow ps:wait -a $APP_NAME -w $WORKLOAD_NAME
|
|
23
|
+
|
|
24
|
+
# Waits for all workloads with a 5-minute timeout.
|
|
25
|
+
timeout 300 cpflow ps:wait -a $APP_NAME
|
|
22
26
|
```
|
|
23
27
|
EX
|
|
24
28
|
|
data/lib/command/run.rb
CHANGED
|
@@ -97,7 +97,7 @@ module Command
|
|
|
97
97
|
|
|
98
98
|
attr_reader :interactive, :detached, :location, :original_workload, :runner_workload,
|
|
99
99
|
:default_image, :default_cpu, :default_memory, :job_timeout, :job_history_limit,
|
|
100
|
-
:container, :
|
|
100
|
+
:container, :job, :replica, :command
|
|
101
101
|
|
|
102
102
|
def call # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
103
103
|
@interactive = config.options[:interactive] || interactive_command?
|
|
@@ -126,10 +126,7 @@ module Command
|
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
create_runner_workload if cp.fetch_workload(runner_workload).nil?
|
|
129
|
-
wait_for_runner_workload_deploy
|
|
130
129
|
update_runner_workload
|
|
131
|
-
wait_for_runner_workload_update if expected_deployed_version
|
|
132
|
-
|
|
133
130
|
start_job
|
|
134
131
|
wait_for_replica_for_job
|
|
135
132
|
|
|
@@ -191,7 +188,7 @@ module Command
|
|
|
191
188
|
}
|
|
192
189
|
|
|
193
190
|
# Create runner workload
|
|
194
|
-
cp.apply_hash("kind" => "workload", "name" => runner_workload, "spec" => spec)
|
|
191
|
+
cp.apply_hash({ "kind" => "workload", "name" => runner_workload, "spec" => spec }, wait: true)
|
|
195
192
|
end
|
|
196
193
|
end
|
|
197
194
|
|
|
@@ -242,21 +239,7 @@ module Command
|
|
|
242
239
|
return unless should_update
|
|
243
240
|
|
|
244
241
|
step("Updating runner workload '#{runner_workload}'") do
|
|
245
|
-
|
|
246
|
-
@expected_deployed_version = (cp.cron_workload_deployed_version(runner_workload) || 0) + 1
|
|
247
|
-
cp.apply_hash("kind" => "workload", "name" => runner_workload, "spec" => spec)
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def wait_for_runner_workload_deploy
|
|
252
|
-
step("Waiting for runner workload '#{runner_workload}' to be deployed", retry_on_failure: true) do
|
|
253
|
-
!cp.cron_workload_deployed_version(runner_workload).nil?
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def wait_for_runner_workload_update
|
|
258
|
-
step("Waiting for runner workload '#{runner_workload}' to be updated", retry_on_failure: true) do
|
|
259
|
-
(cp.cron_workload_deployed_version(runner_workload) || 0) >= expected_deployed_version
|
|
242
|
+
cp.apply_hash({ "kind" => "workload", "name" => runner_workload, "spec" => spec }, wait: true)
|
|
260
243
|
end
|
|
261
244
|
end
|
|
262
245
|
|
|
@@ -274,7 +257,7 @@ module Command
|
|
|
274
257
|
def wait_for_replica_for_job
|
|
275
258
|
step("Waiting for replica to start, which runs job '#{job}'", retry_on_failure: true) do
|
|
276
259
|
result = cp.fetch_workload_replicas(runner_workload, location: location)
|
|
277
|
-
@replica = result
|
|
260
|
+
@replica = result&.dig("items")&.find { |item| item.include?(job) }
|
|
278
261
|
|
|
279
262
|
replica || false
|
|
280
263
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Command
|
|
4
|
+
module Terraform
|
|
5
|
+
class Base < Command::Base
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def templates
|
|
9
|
+
parser = TemplateParser.new(self)
|
|
10
|
+
template_files = Dir["#{parser.template_dir}/*.yml"]
|
|
11
|
+
|
|
12
|
+
if template_files.empty?
|
|
13
|
+
Shell.warn("No templates found in #{parser.template_dir}")
|
|
14
|
+
return []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
parser.parse(template_files)
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
Shell.warn("Error parsing templates: #{e.message}")
|
|
20
|
+
[]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def terraform_dir
|
|
24
|
+
@terraform_dir ||= begin
|
|
25
|
+
full_path = config.options.fetch(:dir, Cpflow.root_path.join("terraform"))
|
|
26
|
+
Pathname.new(full_path).tap do |path|
|
|
27
|
+
FileUtils.mkdir_p(path)
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
Shell.abort("Invalid directory: #{e.message}")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Command
|
|
4
|
+
module Terraform
|
|
5
|
+
class Generate < Base
|
|
6
|
+
SUBCOMMAND_NAME = "terraform"
|
|
7
|
+
NAME = "generate"
|
|
8
|
+
OPTIONS = [
|
|
9
|
+
app_option,
|
|
10
|
+
dir_option
|
|
11
|
+
].freeze
|
|
12
|
+
DESCRIPTION = "Generates terraform configuration files"
|
|
13
|
+
LONG_DESCRIPTION = <<~DESC
|
|
14
|
+
- Generates terraform configuration files based on `controlplane.yml` and `templates/` config
|
|
15
|
+
DESC
|
|
16
|
+
WITH_INFO_HEADER = false
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
Array(config.app || config.apps.keys).each do |app|
|
|
20
|
+
config.instance_variable_set(:@app, app.to_s)
|
|
21
|
+
generate_app_config
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def generate_app_config
|
|
28
|
+
copy_workload_module
|
|
29
|
+
|
|
30
|
+
terraform_app_dir = cleaned_terraform_app_dir
|
|
31
|
+
generate_provider_configs(terraform_app_dir)
|
|
32
|
+
|
|
33
|
+
templates.each do |template|
|
|
34
|
+
TerraformConfig::Generator.new(config: config, template: template).tf_configs.each do |filename, tf_config|
|
|
35
|
+
File.write(terraform_app_dir.join(filename), tf_config.to_tf, mode: "a+")
|
|
36
|
+
end
|
|
37
|
+
rescue TerraformConfig::Generator::InvalidTemplateError => e
|
|
38
|
+
Shell.warn(e.message)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def copy_workload_module
|
|
43
|
+
FileUtils.copy_entry(
|
|
44
|
+
Cpflow.root_path.join("lib/core/terraform_config/workload"),
|
|
45
|
+
terraform_dir.join("workload")
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def generate_provider_configs(terraform_app_dir)
|
|
50
|
+
generate_required_providers(terraform_app_dir)
|
|
51
|
+
generate_providers(terraform_app_dir)
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
Shell.abort("Failed to generate provider config files: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def generate_required_providers(terraform_app_dir)
|
|
57
|
+
File.write(terraform_app_dir.join("required_providers.tf"), required_cpln_provider.to_tf)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def generate_providers(terraform_app_dir)
|
|
61
|
+
cpln_provider = TerraformConfig::Provider.new(name: "cpln", org: config.org)
|
|
62
|
+
File.write(terraform_app_dir.join("providers.tf"), cpln_provider.to_tf)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def required_cpln_provider
|
|
66
|
+
TerraformConfig::RequiredProvider.new(
|
|
67
|
+
name: "cpln",
|
|
68
|
+
org: config.org,
|
|
69
|
+
source: "controlplane-com/cpln",
|
|
70
|
+
version: "~> 1.0"
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def cleaned_terraform_app_dir
|
|
75
|
+
full_path = terraform_dir.join(config.app)
|
|
76
|
+
|
|
77
|
+
unless File.expand_path(full_path).include?(Cpflow.root_path.to_s)
|
|
78
|
+
Shell.abort("Directory to save terraform configuration files cannot be outside of current directory")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if Dir.exist?(full_path)
|
|
82
|
+
clean_terraform_app_dir(full_path)
|
|
83
|
+
else
|
|
84
|
+
FileUtils.mkdir_p(full_path)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
full_path
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def clean_terraform_app_dir(terraform_app_dir)
|
|
91
|
+
Dir.children(terraform_app_dir).each do |child|
|
|
92
|
+
next if child == ".terraform.lock.hcl"
|
|
93
|
+
|
|
94
|
+
FileUtils.rm_rf(terraform_app_dir.join(child))
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Command
|
|
4
|
+
module Terraform
|
|
5
|
+
class Import < Base
|
|
6
|
+
SUBCOMMAND_NAME = "terraform"
|
|
7
|
+
NAME = "import"
|
|
8
|
+
OPTIONS = [
|
|
9
|
+
app_option,
|
|
10
|
+
dir_option
|
|
11
|
+
].freeze
|
|
12
|
+
DESCRIPTION = "Imports terraform resources"
|
|
13
|
+
LONG_DESCRIPTION = <<~DESC
|
|
14
|
+
- Imports terraform resources from the generated configuration files
|
|
15
|
+
DESC
|
|
16
|
+
WITH_INFO_HEADER = false
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
Array(config.app || config.apps.keys).each do |app|
|
|
20
|
+
config.instance_variable_set(:@app, app.to_s)
|
|
21
|
+
|
|
22
|
+
Dir.chdir(terraform_app_dir) do
|
|
23
|
+
run_terraform_init
|
|
24
|
+
|
|
25
|
+
resources.each do |resource|
|
|
26
|
+
run_terraform_import(resource[:address], resource[:id])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def run_terraform_init
|
|
35
|
+
result = Shell.cmd("terraform", "init", capture_stderr: true)
|
|
36
|
+
|
|
37
|
+
if result[:success]
|
|
38
|
+
Shell.info(result[:output])
|
|
39
|
+
else
|
|
40
|
+
Shell.abort("Failed to initialize terraform - #{result[:output]}")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run_terraform_import(address, id)
|
|
45
|
+
result = Shell.cmd("terraform", "import", address, id, capture_stderr: true)
|
|
46
|
+
Shell.info(result[:output])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resources
|
|
50
|
+
tf_configs.filter_map do |tf_config|
|
|
51
|
+
next unless tf_config.importable?
|
|
52
|
+
|
|
53
|
+
{ address: tf_config.reference, id: resource_id(tf_config) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def tf_configs
|
|
58
|
+
templates.flat_map do |template|
|
|
59
|
+
TerraformConfig::Generator.new(config: config, template: template).tf_configs.values
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def resource_id(tf_config)
|
|
64
|
+
case tf_config
|
|
65
|
+
when TerraformConfig::Gvc, TerraformConfig::Policy,
|
|
66
|
+
TerraformConfig::Secret, TerraformConfig::Agent,
|
|
67
|
+
TerraformConfig::AuditContext
|
|
68
|
+
tf_config.name
|
|
69
|
+
else
|
|
70
|
+
"#{config.app}:#{tf_config.name}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def terraform_app_dir
|
|
75
|
+
terraform_dir.join(config.app)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/core/config.rb
CHANGED
|
@@ -25,7 +25,7 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
|
25
25
|
return unless trace_mode
|
|
26
26
|
|
|
27
27
|
ControlplaneApiDirect.trace = trace_mode
|
|
28
|
-
Shell.warn("Trace mode is enabled
|
|
28
|
+
Shell.warn("Trace mode is enabled. Sensitive data is redacted, but please review output before sharing.")
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def org
|
data/lib/core/controlplane.rb
CHANGED
|
@@ -192,7 +192,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
192
192
|
end
|
|
193
193
|
|
|
194
194
|
def fetch_workload_replicas(workload, location:)
|
|
195
|
-
cmd = "cpln workload replica get #{workload} #{gvc_org} --location #{location} -o yaml"
|
|
195
|
+
cmd = "cpln workload replica get #{workload} #{gvc_org} --location #{location} -o yaml 2> /dev/null"
|
|
196
196
|
perform_yaml(cmd)
|
|
197
197
|
end
|
|
198
198
|
|
|
@@ -213,8 +213,8 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
213
213
|
end
|
|
214
214
|
end
|
|
215
215
|
|
|
216
|
-
def workload_deployments_ready?(workload, location:, expected_status:)
|
|
217
|
-
deployed_replicas = fetch_workload_replicas(workload, location: location)
|
|
216
|
+
def workload_deployments_ready?(workload, location:, expected_status:) # rubocop:disable Metrics/CyclomaticComplexity
|
|
217
|
+
deployed_replicas = fetch_workload_replicas(workload, location: location)&.dig("items")&.length || 0
|
|
218
218
|
return deployed_replicas.zero? if expected_status == false
|
|
219
219
|
|
|
220
220
|
deployments = fetch_workload_deployments(workload)["items"]
|
|
@@ -404,11 +404,12 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
404
404
|
end
|
|
405
405
|
|
|
406
406
|
# apply
|
|
407
|
-
def apply_template(data) # rubocop:disable Metrics/MethodLength
|
|
407
|
+
def apply_template(data, wait: false) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
408
408
|
Tempfile.create do |f|
|
|
409
409
|
f.write(data)
|
|
410
410
|
f.rewind
|
|
411
411
|
cmd = "cpln apply #{gvc_org} --file #{f.path}"
|
|
412
|
+
cmd += " --ready" if wait && ENV.fetch("DISABLE_APPLY_READY", nil).nil?
|
|
412
413
|
if Shell.tmp_stderr
|
|
413
414
|
cmd += " 2> #{Shell.tmp_stderr.path}" if Shell.should_hide_output?
|
|
414
415
|
|
|
@@ -429,8 +430,8 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
429
430
|
end
|
|
430
431
|
end
|
|
431
432
|
|
|
432
|
-
def apply_hash(data)
|
|
433
|
-
apply_template(data.to_yaml)
|
|
433
|
+
def apply_hash(data, wait: false)
|
|
434
|
+
apply_template(data.to_yaml, wait: wait)
|
|
434
435
|
end
|
|
435
436
|
|
|
436
437
|
def parse_apply_result(result) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
class RedactedDebugOutput
|
|
4
|
+
SAFE_HEADERS = %w[Content-Type Content-Length Accept Host Date Cache-Control Connection].freeze
|
|
5
|
+
HEADER_REGEX = /^([A-Za-z\-]+): (.+)$/.freeze
|
|
6
|
+
|
|
7
|
+
def <<(msg)
|
|
8
|
+
$stdout << redact(msg)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def redact(msg)
|
|
14
|
+
msg.lines.map { |line| redact_line(line) }.join
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def redact_line(line)
|
|
18
|
+
match = line.match(HEADER_REGEX)
|
|
19
|
+
return line.gsub(/[\w\-._]{50,}/, "[REDACTED]") unless match
|
|
20
|
+
|
|
21
|
+
SAFE_HEADERS.any? { |h| h.casecmp(match[1]).zero? } ? line : "#{match[1]}: [REDACTED]\n"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
3
25
|
class ControlplaneApiDirect
|
|
4
26
|
API_METHODS = {
|
|
5
27
|
get: Net::HTTP::Get,
|
|
@@ -37,7 +59,7 @@ class ControlplaneApiDirect
|
|
|
37
59
|
|
|
38
60
|
http = Net::HTTP.new(uri.hostname, uri.port)
|
|
39
61
|
http.use_ssl = uri.scheme == "https"
|
|
40
|
-
http.set_debug_output(
|
|
62
|
+
http.set_debug_output(RedactedDebugOutput.new) if trace
|
|
41
63
|
|
|
42
64
|
response = http.start { |ht| ht.request(request) }
|
|
43
65
|
|
data/lib/core/shell.rb
CHANGED
|
@@ -5,10 +5,6 @@ class Shell
|
|
|
5
5
|
attr_reader :tmp_stderr, :verbose
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
def self.shell
|
|
9
|
-
@shell ||= Thor::Shell::Color.new
|
|
10
|
-
end
|
|
11
|
-
|
|
12
8
|
def self.use_tmp_stderr
|
|
13
9
|
@tmp_stderr = Tempfile.create
|
|
14
10
|
|
|
@@ -35,6 +31,10 @@ class Shell
|
|
|
35
31
|
shell.yes?("#{message} (y/N)")
|
|
36
32
|
end
|
|
37
33
|
|
|
34
|
+
def self.info(message)
|
|
35
|
+
shell.say(message)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
38
|
def self.warn(message)
|
|
39
39
|
Kernel.warn(color("WARNING: #{message}", :yellow))
|
|
40
40
|
end
|
|
@@ -97,4 +97,9 @@ class Shell
|
|
|
97
97
|
exit(ExitCode::INTERRUPT)
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
|
+
|
|
101
|
+
def self.shell
|
|
102
|
+
@shell ||= Thor::Shell::Color.new
|
|
103
|
+
end
|
|
104
|
+
private_class_method :shell
|
|
100
105
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TerraformConfig
|
|
4
|
+
class Agent < Base
|
|
5
|
+
attr_reader :name, :description, :tags
|
|
6
|
+
|
|
7
|
+
def initialize(name:, description: nil, tags: nil)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@name = name
|
|
11
|
+
@description = description
|
|
12
|
+
@tags = tags
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def importable?
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reference
|
|
20
|
+
"cpln_agent.#{name}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_tf
|
|
24
|
+
block :resource, :cpln_agent, name do
|
|
25
|
+
argument :name, name
|
|
26
|
+
argument :description, description, optional: true
|
|
27
|
+
argument :tags, tags, optional: true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TerraformConfig
|
|
4
|
+
class AuditContext < Base
|
|
5
|
+
attr_reader :name, :description, :tags
|
|
6
|
+
|
|
7
|
+
def initialize(name:, description: nil, tags: nil)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@name = name
|
|
11
|
+
@description = description
|
|
12
|
+
@tags = tags
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def importable?
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reference
|
|
20
|
+
"cpln_audit_context.#{name}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_tf
|
|
24
|
+
block :resource, :cpln_audit_context, name do
|
|
25
|
+
argument :name, name
|
|
26
|
+
argument :description, description, optional: true
|
|
27
|
+
argument :tags, tags, optional: true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dsl"
|
|
4
|
+
|
|
5
|
+
module TerraformConfig
|
|
6
|
+
class Base
|
|
7
|
+
include Dsl
|
|
8
|
+
|
|
9
|
+
def importable?
|
|
10
|
+
false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def reference
|
|
14
|
+
raise NotImplementedError if importable?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_tf
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def locals
|
|
22
|
+
{}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TerraformConfig
|
|
4
|
+
module Dsl
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
EXPRESSION_PATTERN = /(var|local|cpln_\w+)\./.freeze
|
|
8
|
+
|
|
9
|
+
def_delegators :current_context, :put, :output
|
|
10
|
+
|
|
11
|
+
def block(name, *labels)
|
|
12
|
+
switch_context do
|
|
13
|
+
put("#{block_declaration(name, labels)} {\n")
|
|
14
|
+
yield
|
|
15
|
+
put("}\n")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# There is extra indent for whole output that needs to be removed
|
|
19
|
+
output.unindent
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def argument(name, value, optional: false, raw: false)
|
|
23
|
+
return if value.nil? && optional
|
|
24
|
+
|
|
25
|
+
content =
|
|
26
|
+
if value.is_a?(Hash)
|
|
27
|
+
operator = raw ? ": " : " = "
|
|
28
|
+
"{\n#{value.map { |n, v| "#{n}#{operator}#{tf_value(v)}" }.join("\n").indent(2)}\n}\n"
|
|
29
|
+
else
|
|
30
|
+
"#{tf_value(value)}\n"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# remove quotes from expression values
|
|
34
|
+
content = content.gsub(/("#{EXPRESSION_PATTERN}.*")/) { ::Regexp.last_match(1)[1...-1] }
|
|
35
|
+
|
|
36
|
+
put("#{name} = #{content}", indent: 2)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def tf_value(value)
|
|
42
|
+
value = value.to_s if value.is_a?(Symbol)
|
|
43
|
+
|
|
44
|
+
case value
|
|
45
|
+
when String
|
|
46
|
+
tf_string_value(value)
|
|
47
|
+
when Hash
|
|
48
|
+
tf_hash_value(value)
|
|
49
|
+
else
|
|
50
|
+
value
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def tf_string_value(value)
|
|
55
|
+
return value if expression?(value)
|
|
56
|
+
return "\"#{value}\"" unless value.include?("\n")
|
|
57
|
+
|
|
58
|
+
"EOF\n#{value.indent(2)}\nEOF"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def tf_hash_value(value)
|
|
62
|
+
JSON.pretty_generate(value.crush)
|
|
63
|
+
.gsub(/"(\w+)":/) { "#{::Regexp.last_match(1)}:" } # remove quotes from keys
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def expression?(value)
|
|
67
|
+
value.match?(/^#{EXPRESSION_PATTERN}/)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def block_declaration(name, labels)
|
|
71
|
+
result = name.to_s
|
|
72
|
+
return result unless labels.any?
|
|
73
|
+
|
|
74
|
+
result + " #{labels.map { |label| tf_value(label) }.join(' ')}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class Context
|
|
78
|
+
attr_accessor :output
|
|
79
|
+
|
|
80
|
+
def initialize
|
|
81
|
+
@output = ""
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def put(content, indent: 0)
|
|
85
|
+
@output += content.to_s.indent(indent)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def switch_context
|
|
90
|
+
old_context = current_context
|
|
91
|
+
@current_context = Context.new
|
|
92
|
+
yield
|
|
93
|
+
ensure
|
|
94
|
+
old_context.put(current_context.output, indent: 2)
|
|
95
|
+
@current_context = old_context
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def current_context
|
|
99
|
+
@current_context ||= Context.new
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|