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,263 @@
|
|
|
1
|
+
variable "containers" {
|
|
2
|
+
type = map(
|
|
3
|
+
object({
|
|
4
|
+
args = optional(list(string))
|
|
5
|
+
command = optional(string)
|
|
6
|
+
cpu = optional(string, "1000m")
|
|
7
|
+
envs = optional(map(string))
|
|
8
|
+
image = string
|
|
9
|
+
inherit_env = optional(bool)
|
|
10
|
+
liveness_probe = optional(
|
|
11
|
+
object({
|
|
12
|
+
exec = optional(
|
|
13
|
+
object({
|
|
14
|
+
command = list(string)
|
|
15
|
+
})
|
|
16
|
+
)
|
|
17
|
+
failure_threshold = optional(number)
|
|
18
|
+
grpc = optional(
|
|
19
|
+
object({
|
|
20
|
+
port = optional(number)
|
|
21
|
+
})
|
|
22
|
+
)
|
|
23
|
+
http_get = optional(
|
|
24
|
+
object({
|
|
25
|
+
http_headers = optional(map(string))
|
|
26
|
+
path = optional(string)
|
|
27
|
+
port = optional(number)
|
|
28
|
+
scheme = optional(string)
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
initial_delay_seconds = optional(number)
|
|
32
|
+
period_seconds = optional(number)
|
|
33
|
+
success_threshold = optional(number)
|
|
34
|
+
timeout_seconds = optional(number)
|
|
35
|
+
tcp_socket = optional(
|
|
36
|
+
object({
|
|
37
|
+
port = optional(number)
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
memory = optional(string, "2048Mi")
|
|
43
|
+
ports = optional(
|
|
44
|
+
list(
|
|
45
|
+
object({
|
|
46
|
+
number = number
|
|
47
|
+
protocol = string
|
|
48
|
+
})
|
|
49
|
+
),
|
|
50
|
+
[],
|
|
51
|
+
)
|
|
52
|
+
post_start_command = optional(string)
|
|
53
|
+
pre_stop_command = optional(string)
|
|
54
|
+
readiness_probe = optional(
|
|
55
|
+
object({
|
|
56
|
+
exec = optional(
|
|
57
|
+
object({
|
|
58
|
+
command = list(string)
|
|
59
|
+
})
|
|
60
|
+
)
|
|
61
|
+
failure_threshold = optional(number)
|
|
62
|
+
grpc = optional(
|
|
63
|
+
object({
|
|
64
|
+
port = optional(number)
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
http_get = optional(
|
|
68
|
+
object({
|
|
69
|
+
http_headers = optional(map(string))
|
|
70
|
+
path = optional(string)
|
|
71
|
+
port = optional(number)
|
|
72
|
+
scheme = optional(string)
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
initial_delay_seconds = optional(number)
|
|
76
|
+
period_seconds = optional(number)
|
|
77
|
+
success_threshold = optional(number)
|
|
78
|
+
timeout_seconds = optional(number)
|
|
79
|
+
tcp_socket = optional(
|
|
80
|
+
object({
|
|
81
|
+
port = optional(number)
|
|
82
|
+
})
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
)
|
|
86
|
+
volumes = optional(
|
|
87
|
+
list(
|
|
88
|
+
object({
|
|
89
|
+
path = string
|
|
90
|
+
uri = string
|
|
91
|
+
})
|
|
92
|
+
),
|
|
93
|
+
[],
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
variable "description" {
|
|
100
|
+
type = string
|
|
101
|
+
default = null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
variable "firewall_spec" {
|
|
105
|
+
type = object({
|
|
106
|
+
external = optional(
|
|
107
|
+
object({
|
|
108
|
+
inbound_allow_cidr = optional(list(string))
|
|
109
|
+
outbound_allow_hostname = optional(list(string))
|
|
110
|
+
outbound_allow_cidr = optional(list(string))
|
|
111
|
+
outbound_allow_port = optional(
|
|
112
|
+
list(
|
|
113
|
+
object({
|
|
114
|
+
number = number
|
|
115
|
+
protocol = optional(string, "tcp")
|
|
116
|
+
})
|
|
117
|
+
),
|
|
118
|
+
[]
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
)
|
|
122
|
+
internal = optional(
|
|
123
|
+
object({
|
|
124
|
+
inbound_allow_type = optional(string)
|
|
125
|
+
inbound_allow_workload = optional(list(string))
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
default = null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
variable "gvc" {
|
|
133
|
+
type = string
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
variable "identity_link" {
|
|
137
|
+
type = string
|
|
138
|
+
default = null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
variable "job" {
|
|
142
|
+
type = object({
|
|
143
|
+
active_deadline_seconds = optional(number)
|
|
144
|
+
concurrency_policy = optional(string, "Forbid")
|
|
145
|
+
history_limit = optional(number, 5)
|
|
146
|
+
restart_policy = optional(string, "Never")
|
|
147
|
+
schedule = string
|
|
148
|
+
})
|
|
149
|
+
default = null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
variable "load_balancer" {
|
|
153
|
+
type = object({
|
|
154
|
+
direct = optional(
|
|
155
|
+
object({
|
|
156
|
+
enabled = bool
|
|
157
|
+
port = optional(
|
|
158
|
+
list(
|
|
159
|
+
object({
|
|
160
|
+
container_port = optional(number)
|
|
161
|
+
external_port = number
|
|
162
|
+
protocol = string
|
|
163
|
+
scheme = optional(string)
|
|
164
|
+
})
|
|
165
|
+
),
|
|
166
|
+
[]
|
|
167
|
+
)
|
|
168
|
+
})
|
|
169
|
+
)
|
|
170
|
+
geo_location = optional(
|
|
171
|
+
object({
|
|
172
|
+
enabled = optional(bool)
|
|
173
|
+
headers = optional(
|
|
174
|
+
object({
|
|
175
|
+
asn = optional(string)
|
|
176
|
+
city = optional(string)
|
|
177
|
+
country = optional(string)
|
|
178
|
+
region = optional(string)
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
default = null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
variable "local_options" {
|
|
188
|
+
type = object({
|
|
189
|
+
autoscaling = optional(
|
|
190
|
+
object({
|
|
191
|
+
metric = optional(string)
|
|
192
|
+
metric_percentile = optional(string)
|
|
193
|
+
target = optional(number)
|
|
194
|
+
max_scale = optional(number)
|
|
195
|
+
min_scale = optional(number)
|
|
196
|
+
scale_to_zero_delay = optional(number)
|
|
197
|
+
max_concurrency = optional(number)
|
|
198
|
+
})
|
|
199
|
+
)
|
|
200
|
+
location = string
|
|
201
|
+
capacity_ai = optional(bool, true)
|
|
202
|
+
debug = optional(bool, false)
|
|
203
|
+
suspend = optional(bool, false)
|
|
204
|
+
timeout_seconds = optional(number, 5)
|
|
205
|
+
})
|
|
206
|
+
default = null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
variable "name" {
|
|
210
|
+
type = string
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
variable "options" {
|
|
214
|
+
type = object({
|
|
215
|
+
autoscaling = optional(
|
|
216
|
+
object({
|
|
217
|
+
max_concurrency = optional(number)
|
|
218
|
+
max_scale = optional(number)
|
|
219
|
+
metric = optional(string)
|
|
220
|
+
metric_percentile = optional(string)
|
|
221
|
+
min_scale = optional(number)
|
|
222
|
+
scale_to_zero_delay = optional(number)
|
|
223
|
+
target = optional(number)
|
|
224
|
+
})
|
|
225
|
+
)
|
|
226
|
+
capacity_ai = optional(bool, true)
|
|
227
|
+
debug = optional(bool, false)
|
|
228
|
+
suspend = optional(bool, false)
|
|
229
|
+
timeout_seconds = optional(number, 5)
|
|
230
|
+
})
|
|
231
|
+
default = null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
variable "rollout_options" {
|
|
235
|
+
type = object({
|
|
236
|
+
max_surge_replicas = optional(string)
|
|
237
|
+
max_unavailable_replicas = optional(string)
|
|
238
|
+
min_ready_seconds = optional(number)
|
|
239
|
+
scaling_policy = optional(string, "OrderedReady")
|
|
240
|
+
})
|
|
241
|
+
default = null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
variable "security_options" {
|
|
245
|
+
type = object({
|
|
246
|
+
file_system_group_id = optional(number)
|
|
247
|
+
})
|
|
248
|
+
default = null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
variable "support_dynamic_tags" {
|
|
252
|
+
type = bool
|
|
253
|
+
default = false
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
variable "tags" {
|
|
257
|
+
type = map(string)
|
|
258
|
+
default = {}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
variable "type" {
|
|
262
|
+
type = string
|
|
263
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TerraformConfig
|
|
4
|
+
class Workload < Base # rubocop:disable Metrics/ClassLength
|
|
5
|
+
RAW_ARGS = %i[
|
|
6
|
+
containers options local_options rollout_options security_options
|
|
7
|
+
firewall_spec load_balancer job
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
OPTIONS_KEYS = %i[autoscaling capacity_ai suspend timeout_seconds].freeze
|
|
11
|
+
LOCAL_OPTIONS_KEYS = (OPTIONS_KEYS + %i[location]).freeze
|
|
12
|
+
ROLLOUT_OPTIONS_KEYS = %i[min_ready_seconds max_unavailable_replicas max_surge_replicas scaling_policy].freeze
|
|
13
|
+
SECURITY_OPTIONS_KEYS = %i[file_system_group_id].freeze
|
|
14
|
+
LOAD_BALANCER_KEYS = %i[direct geo_location].freeze
|
|
15
|
+
FIREWALL_SPEC_KEYS = %i[internal external].freeze
|
|
16
|
+
VOLUME_KEYS = %i[uri path].freeze
|
|
17
|
+
JOB_KEYS = %i[schedule concurrency_policy history_limit restart_policy active_deadline_seconds].freeze
|
|
18
|
+
LIVENESS_PROBE_KEYS = %i[
|
|
19
|
+
exec http_get tcp_socket grpc
|
|
20
|
+
failure_threshold initial_delay_seconds period_seconds success_threshold timeout_seconds
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
attr_reader :type, :name, :gvc, :containers,
|
|
24
|
+
:description, :tags, :support_dynamic_tags, :firewall_spec, :identity_link,
|
|
25
|
+
:options, :local_options, :rollout_options, :security_options, :load_balancer, :job
|
|
26
|
+
|
|
27
|
+
def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
28
|
+
type:,
|
|
29
|
+
gvc:,
|
|
30
|
+
name:,
|
|
31
|
+
containers:,
|
|
32
|
+
description: nil,
|
|
33
|
+
tags: nil,
|
|
34
|
+
support_dynamic_tags: false,
|
|
35
|
+
firewall_spec: nil,
|
|
36
|
+
identity_link: nil,
|
|
37
|
+
options: nil,
|
|
38
|
+
local_options: nil,
|
|
39
|
+
rollout_options: nil,
|
|
40
|
+
security_options: nil,
|
|
41
|
+
load_balancer: nil,
|
|
42
|
+
job: nil
|
|
43
|
+
)
|
|
44
|
+
super()
|
|
45
|
+
|
|
46
|
+
@type = type
|
|
47
|
+
@gvc = gvc
|
|
48
|
+
|
|
49
|
+
@name = name
|
|
50
|
+
@description = description
|
|
51
|
+
@tags = tags
|
|
52
|
+
|
|
53
|
+
@containers = containers
|
|
54
|
+
@firewall_spec = firewall_spec
|
|
55
|
+
@identity_link = identity_link
|
|
56
|
+
|
|
57
|
+
@options = options
|
|
58
|
+
@local_options = local_options
|
|
59
|
+
@rollout_options = rollout_options
|
|
60
|
+
@security_options = security_options
|
|
61
|
+
|
|
62
|
+
@load_balancer = load_balancer
|
|
63
|
+
@support_dynamic_tags = support_dynamic_tags
|
|
64
|
+
@job = job
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def importable?
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def reference
|
|
72
|
+
"module.#{name}.cpln_workload.workload"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_tf
|
|
76
|
+
block :module, name do
|
|
77
|
+
argument :source, "../workload"
|
|
78
|
+
|
|
79
|
+
argument :type, type
|
|
80
|
+
argument :name, name
|
|
81
|
+
argument :gvc, gvc
|
|
82
|
+
argument :identity_link, identity_link, optional: true
|
|
83
|
+
argument :support_dynamic_tags, support_dynamic_tags, optional: true
|
|
84
|
+
|
|
85
|
+
RAW_ARGS.each { |arg_name| argument arg_name, send(:"#{arg_name}_arg"), raw: true, optional: true }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def locals
|
|
90
|
+
containers.reduce({}) do |result, container|
|
|
91
|
+
envs = container[:env].to_h { |env_var| [env_var[:name], env_var[:value]] }
|
|
92
|
+
next result if envs.empty?
|
|
93
|
+
|
|
94
|
+
envs_name = :"#{container.fetch(:name)}_envs"
|
|
95
|
+
result.merge("#{envs_name}.tf" => LocalVariable.new(envs_name => envs))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def containers_arg
|
|
102
|
+
containers.reduce({}) { |result, container| result.merge(container_args(container)) }.crush
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def container_args(container) # rubocop:disable Metrics/MethodLength
|
|
106
|
+
container_name = container.fetch(:name)
|
|
107
|
+
|
|
108
|
+
args = container.slice(:args, :command, :image, :cpu, :memory).merge(
|
|
109
|
+
post_start: container.dig(:lifecycle, :post_start, :exec, :command),
|
|
110
|
+
pre_stop: container.dig(:lifecycle, :pre_stop, :exec, :command),
|
|
111
|
+
inherit_env: container.fetch(:inherit_env, nil),
|
|
112
|
+
envs: ("local.#{container_name}_envs" if container[:env]&.any?),
|
|
113
|
+
ports: container.fetch(:ports, nil),
|
|
114
|
+
readiness_probe: container.fetch(:readiness_probe, nil)&.slice(*LIVENESS_PROBE_KEYS),
|
|
115
|
+
liveness_probe: container.fetch(:liveness_probe, nil)&.slice(*LIVENESS_PROBE_KEYS),
|
|
116
|
+
volumes: container.fetch(:volumes, nil)&.map { |volume| volume.slice(*VOLUME_KEYS) }
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
{ container_name => args }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
RAW_ARGS.each do |spec|
|
|
123
|
+
next if spec == :containers
|
|
124
|
+
|
|
125
|
+
define_method("#{spec}_arg") do
|
|
126
|
+
return if send(spec).nil?
|
|
127
|
+
|
|
128
|
+
send(spec).slice(*self.class.const_get("#{spec.upcase}_KEYS"))
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
data/lib/cpflow/version.rb
CHANGED
data/lib/cpflow.rb
CHANGED
|
@@ -17,6 +17,9 @@ require_relative "constants/exit_code"
|
|
|
17
17
|
|
|
18
18
|
# We need to require base before all commands, since the commands inherit from it
|
|
19
19
|
require_relative "command/base"
|
|
20
|
+
require_relative "command/terraform/base"
|
|
21
|
+
# We need to require base terraform config before all commands, since the terraform configs inherit from it
|
|
22
|
+
require_relative "core/terraform_config/base"
|
|
20
23
|
|
|
21
24
|
modules = Dir["#{__dir__}/**/*.rb"].reject do |file|
|
|
22
25
|
file == __FILE__ || file.end_with?("base.rb")
|
|
@@ -33,15 +36,22 @@ end
|
|
|
33
36
|
|
|
34
37
|
require_relative "patches/thor"
|
|
35
38
|
|
|
39
|
+
require_relative "patches/string"
|
|
40
|
+
|
|
36
41
|
module Cpflow
|
|
37
42
|
class Error < StandardError; end
|
|
38
43
|
|
|
44
|
+
def self.root_path
|
|
45
|
+
Pathname.new(File.expand_path("../", __dir__))
|
|
46
|
+
end
|
|
47
|
+
|
|
39
48
|
class Cli < Thor # rubocop:disable Metrics/ClassLength
|
|
40
49
|
package_name "cpflow"
|
|
41
50
|
default_task :no_command
|
|
42
51
|
|
|
43
52
|
def self.start(*args)
|
|
44
53
|
ENV["CPLN_SKIP_UPDATE_CHECK"] = "true"
|
|
54
|
+
ENV["NODE_NO_WARNINGS"] = "1"
|
|
45
55
|
|
|
46
56
|
check_cpln_version
|
|
47
57
|
check_cpflow_version
|
|
@@ -95,12 +105,21 @@ module Cpflow
|
|
|
95
105
|
def self.fix_help_option
|
|
96
106
|
help_mappings = Thor::HELP_MAPPINGS + ["help"]
|
|
97
107
|
matches = help_mappings & ARGV
|
|
108
|
+
|
|
109
|
+
# Help option works correctly for subcommands
|
|
110
|
+
return if matches && subcommand?
|
|
111
|
+
|
|
98
112
|
matches.each do |match|
|
|
99
113
|
ARGV.delete(match)
|
|
100
114
|
ARGV.unshift(match)
|
|
101
115
|
end
|
|
102
116
|
end
|
|
103
117
|
|
|
118
|
+
def self.subcommand?
|
|
119
|
+
(subcommand_names & ARGV).any?
|
|
120
|
+
end
|
|
121
|
+
private_class_method :subcommand?
|
|
122
|
+
|
|
104
123
|
# Needed to silence deprecation warning
|
|
105
124
|
def self.exit_on_failure?
|
|
106
125
|
true
|
|
@@ -131,6 +150,10 @@ module Cpflow
|
|
|
131
150
|
::Command::Base.all_commands.merge(deprecated_commands)
|
|
132
151
|
end
|
|
133
152
|
|
|
153
|
+
def self.subcommand_names
|
|
154
|
+
Dir["#{__dir__}/command/*"].filter_map { |name| File.basename(name) if File.directory?(name) }
|
|
155
|
+
end
|
|
156
|
+
|
|
134
157
|
def self.process_option_params(params)
|
|
135
158
|
# Ensures that if no value is provided for a non-boolean option (e.g., `cpflow command --option`),
|
|
136
159
|
# it defaults to an empty string instead of the option name (which is the default Thor behavior)
|
|
@@ -139,8 +162,21 @@ module Cpflow
|
|
|
139
162
|
params
|
|
140
163
|
end
|
|
141
164
|
|
|
165
|
+
def self.klass_for(subcommand_name)
|
|
166
|
+
klass_name = subcommand_name.to_s.split("-").map(&:capitalize).join
|
|
167
|
+
full_klass_name = "Cpflow::#{klass_name}"
|
|
168
|
+
return const_get(full_klass_name) if const_defined?(full_klass_name)
|
|
169
|
+
|
|
170
|
+
Cpflow.const_set(klass_name, Class.new(BaseSubCommand)).tap do |subcommand_klass|
|
|
171
|
+
desc(subcommand_name, "#{subcommand_name.capitalize} commands")
|
|
172
|
+
subcommand(subcommand_name, subcommand_klass)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
private_class_method :klass_for
|
|
176
|
+
|
|
142
177
|
@commands_with_required_options = []
|
|
143
178
|
@commands_with_extra_options = []
|
|
179
|
+
cli_package_name = @package_name
|
|
144
180
|
|
|
145
181
|
::Command::Base.common_options.each do |option|
|
|
146
182
|
params = process_option_params(option[:params])
|
|
@@ -151,6 +187,7 @@ module Cpflow
|
|
|
151
187
|
deprecated = deprecated_commands[command_key]
|
|
152
188
|
|
|
153
189
|
name = command_class::NAME
|
|
190
|
+
subcommand_name = command_class::SUBCOMMAND_NAME
|
|
154
191
|
name_for_method = deprecated ? command_key : name.tr("-", "_")
|
|
155
192
|
usage = command_class::USAGE.empty? ? name : command_class::USAGE
|
|
156
193
|
requires_args = command_class::REQUIRES_ARGS
|
|
@@ -170,21 +207,26 @@ module Cpflow
|
|
|
170
207
|
# so we store it here to be able to use it
|
|
171
208
|
raise_args_error = ->(*args) { handle_argument_error(commands[name_for_method], ArgumentError, *args) }
|
|
172
209
|
|
|
173
|
-
desc(usage, description, hide: hide)
|
|
174
|
-
long_desc(long_description)
|
|
175
|
-
|
|
176
|
-
command_options.each do |option|
|
|
177
|
-
params = process_option_params(option[:params])
|
|
178
|
-
method_option(option[:name], **params)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
210
|
# We'll handle required options manually in `Config`
|
|
182
211
|
required_options = command_options.select { |option| option[:params][:required] }.map { |option| option[:name] }
|
|
183
212
|
@commands_with_required_options.push(name_for_method.to_sym) if required_options.any?
|
|
184
213
|
|
|
185
214
|
@commands_with_extra_options.push(name_for_method.to_sym) if accepts_extra_options
|
|
186
215
|
|
|
187
|
-
|
|
216
|
+
klass = subcommand_name ? klass_for(subcommand_name) : self
|
|
217
|
+
|
|
218
|
+
klass.class_eval do
|
|
219
|
+
package_name(cli_package_name) if subcommand_name
|
|
220
|
+
desc(usage, description, hide: hide)
|
|
221
|
+
long_desc(long_description)
|
|
222
|
+
|
|
223
|
+
command_options.each do |option|
|
|
224
|
+
params = Cpflow::Cli.process_option_params(option[:params])
|
|
225
|
+
method_option(option[:name], **params)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
klass.define_method(name_for_method) do |*provided_args| # rubocop:disable Metrics/BlockLength, Metrics/MethodLength
|
|
188
230
|
if deprecated
|
|
189
231
|
normalized_old_name = ::Helpers.normalize_command_name(command_key)
|
|
190
232
|
::Shell.warn_deprecated("Command '#{normalized_old_name}' is deprecated, " \
|
|
@@ -133,7 +133,7 @@ spec:
|
|
|
133
133
|
value: cpln://secret/postgres-poc-credentials.password
|
|
134
134
|
- name: POSTGRES_USER #The name of the default user
|
|
135
135
|
value: cpln://secret/postgres-poc-credentials.username
|
|
136
|
-
name:
|
|
136
|
+
name: postgres
|
|
137
137
|
image: postgres:15
|
|
138
138
|
command: /bin/bash
|
|
139
139
|
args:
|
data/lib/patches/hash.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hash
|
|
4
|
+
# Copied from Rails
|
|
5
|
+
def deep_symbolize_keys
|
|
6
|
+
deep_transform_keys { |key| key.to_sym rescue key } # rubocop:disable Style/RescueModifier
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def deep_underscore_keys
|
|
10
|
+
deep_transform_keys do |key|
|
|
11
|
+
underscored = key.to_s.underscore
|
|
12
|
+
key.is_a?(Symbol) ? underscored.to_sym : underscored
|
|
13
|
+
rescue StandardError
|
|
14
|
+
key
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def crush
|
|
19
|
+
crushed = each_with_object({}) do |(key, value), hash|
|
|
20
|
+
crushed_value = value.respond_to?(:crush) ? value.crush : value
|
|
21
|
+
hash[key] = crushed_value unless crushed_value.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
crushed unless crushed.empty?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Copied from Rails
|
|
30
|
+
def deep_transform_keys(&block)
|
|
31
|
+
deep_transform_keys_in_object(self, &block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Copied from Rails
|
|
35
|
+
def deep_transform_keys_in_object(object, &block)
|
|
36
|
+
case object
|
|
37
|
+
when Hash
|
|
38
|
+
object.each_with_object(self.class.new) do |(key, value), result|
|
|
39
|
+
result[yield(key)] = deep_transform_keys_in_object(value, &block)
|
|
40
|
+
end
|
|
41
|
+
when Array
|
|
42
|
+
object.map { |e| deep_transform_keys_in_object(e, &block) }
|
|
43
|
+
else
|
|
44
|
+
object
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Style/OptionalBooleanParameter, Lint/UnderscorePrefixedVariableName
|
|
4
|
+
class String
|
|
5
|
+
# Copied from Rails
|
|
6
|
+
def indent(amount, indent_string = nil, indent_empty_lines = false)
|
|
7
|
+
dup.tap { |_| _.indent!(amount, indent_string, indent_empty_lines) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Copied from Rails
|
|
11
|
+
def indent!(amount, indent_string = nil, indent_empty_lines = false)
|
|
12
|
+
indent_string = indent_string || self[/^[ \t]/] || " "
|
|
13
|
+
re = indent_empty_lines ? /^/ : /^(?!$)/
|
|
14
|
+
gsub!(re, indent_string * amount)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def unindent
|
|
18
|
+
gsub(/^#{scan(/^[ \t]+(?=\S)/).min}/, "")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Copied from Rails
|
|
22
|
+
def underscore
|
|
23
|
+
gsub("::", "/").gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').tr("-", "_").downcase
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Covers only simple cases and used for pluralizing controlplane template kinds (`gvc`, `secret`, `policy`, etc.)
|
|
27
|
+
def pluralize
|
|
28
|
+
return self if empty?
|
|
29
|
+
return "#{self[...-1]}ies" if end_with?("y")
|
|
30
|
+
|
|
31
|
+
"#{self}s"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
# rubocop:enable Style/OptionalBooleanParameter, Lint/UnderscorePrefixedVariableName
|
data/script/update_command_docs
CHANGED
|
@@ -12,12 +12,16 @@ commands.keys.sort.each do |command_key|
|
|
|
12
12
|
next if command_class::HIDE
|
|
13
13
|
|
|
14
14
|
name = command_class::NAME
|
|
15
|
-
|
|
15
|
+
subcommand_name = command_class::SUBCOMMAND_NAME
|
|
16
|
+
|
|
17
|
+
full_command = [subcommand_name, name].compact.join(" ")
|
|
18
|
+
|
|
19
|
+
usage = command_class::USAGE.empty? ? full_command : command_class::USAGE
|
|
16
20
|
options = command_class::OPTIONS
|
|
17
21
|
long_description = command_class::LONG_DESCRIPTION
|
|
18
22
|
examples = command_class::EXAMPLES
|
|
19
23
|
|
|
20
|
-
command_str = "### `#{
|
|
24
|
+
command_str = "### `#{full_command}`\n\n"
|
|
21
25
|
command_str += "#{long_description.strip}\n\n"
|
|
22
26
|
|
|
23
27
|
if examples.empty?
|