cpflow 4.1.0 → 4.1.1

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/CHANGELOG.md +4 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +7 -7
  6. data/docs/commands.md +16 -0
  7. data/docs/terraform/details.md +415 -0
  8. data/docs/terraform/example/.controlplane/controlplane.yml +29 -0
  9. data/docs/terraform/example/.controlplane/templates/app.yml +38 -0
  10. data/docs/terraform/example/.controlplane/templates/postgres.yml +30 -0
  11. data/docs/terraform/example/.controlplane/templates/rails.yml +26 -0
  12. data/docs/terraform/overview.md +105 -0
  13. data/lib/command/base.rb +29 -5
  14. data/lib/command/base_sub_command.rb +15 -0
  15. data/lib/command/generate.rb +1 -1
  16. data/lib/command/ps.rb +1 -1
  17. data/lib/command/ps_stop.rb +2 -1
  18. data/lib/command/run.rb +1 -1
  19. data/lib/command/terraform/base.rb +35 -0
  20. data/lib/command/terraform/generate.rb +99 -0
  21. data/lib/command/terraform/import.rb +79 -0
  22. data/lib/core/controlplane.rb +3 -3
  23. data/lib/core/shell.rb +9 -4
  24. data/lib/core/terraform_config/agent.rb +31 -0
  25. data/lib/core/terraform_config/audit_context.rb +31 -0
  26. data/lib/core/terraform_config/base.rb +25 -0
  27. data/lib/core/terraform_config/dsl.rb +102 -0
  28. data/lib/core/terraform_config/generator.rb +184 -0
  29. data/lib/core/terraform_config/gvc.rb +63 -0
  30. data/lib/core/terraform_config/identity.rb +35 -0
  31. data/lib/core/terraform_config/local_variable.rb +30 -0
  32. data/lib/core/terraform_config/policy.rb +151 -0
  33. data/lib/core/terraform_config/provider.rb +22 -0
  34. data/lib/core/terraform_config/required_provider.rb +23 -0
  35. data/lib/core/terraform_config/secret.rb +138 -0
  36. data/lib/core/terraform_config/volume_set.rb +155 -0
  37. data/lib/core/terraform_config/workload/main.tf +316 -0
  38. data/lib/core/terraform_config/workload/required_providers.tf +8 -0
  39. data/lib/core/terraform_config/workload/variables.tf +263 -0
  40. data/lib/core/terraform_config/workload.rb +132 -0
  41. data/lib/cpflow/version.rb +1 -1
  42. data/lib/cpflow.rb +50 -9
  43. data/lib/generator_templates/templates/postgres.yml +1 -1
  44. data/lib/patches/array.rb +8 -0
  45. data/lib/patches/hash.rb +47 -0
  46. data/lib/patches/string.rb +34 -0
  47. data/script/update_command_docs +6 -2
  48. metadata +33 -3
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformConfig
4
+ class Generator # rubocop:disable Metrics/ClassLength
5
+ SUPPORTED_TEMPLATE_KINDS = %w[gvc secret identity policy volumeset workload auditctx agent].freeze
6
+ WORKLOAD_SPEC_KEYS = %i[
7
+ type
8
+ containers
9
+ identity_link
10
+ default_options
11
+ local_options
12
+ rollout_options
13
+ security_options
14
+ load_balancer
15
+ firewall_config
16
+ support_dynamic_tags
17
+ job
18
+ ].freeze
19
+
20
+ class InvalidTemplateError < ArgumentError; end
21
+
22
+ attr_reader :config, :template
23
+
24
+ def initialize(config:, template:)
25
+ @config = config
26
+ @template = template.deep_underscore_keys.deep_symbolize_keys
27
+ validate_template_kind!
28
+ end
29
+
30
+ def tf_configs
31
+ tf_config.locals.merge(filename => tf_config)
32
+ end
33
+
34
+ private
35
+
36
+ def validate_template_kind!
37
+ return if SUPPORTED_TEMPLATE_KINDS.include?(kind)
38
+
39
+ raise InvalidTemplateError, "Unsupported template kind: #{kind}"
40
+ end
41
+
42
+ def filename
43
+ case kind
44
+ when "gvc"
45
+ "gvc.tf"
46
+ when "workload"
47
+ "#{template[:name]}.tf"
48
+ when "auditctx"
49
+ "audit_contexts.tf"
50
+ else
51
+ "#{kind.pluralize}.tf"
52
+ end
53
+ end
54
+
55
+ def tf_config
56
+ @tf_config ||= config_class.new(**config_params)
57
+ end
58
+
59
+ def config_class
60
+ case kind
61
+ when "volumeset"
62
+ TerraformConfig::VolumeSet
63
+ when "auditctx"
64
+ TerraformConfig::AuditContext
65
+ else
66
+ TerraformConfig.const_get(kind.capitalize)
67
+ end
68
+ end
69
+
70
+ def config_params
71
+ send("#{kind}_config_params")
72
+ end
73
+
74
+ def gvc_config_params
75
+ template
76
+ .slice(:name, :description, :tags)
77
+ .merge(
78
+ env: gvc_env,
79
+ pull_secrets: gvc_pull_secrets,
80
+ locations: gvc_locations,
81
+ domain: template.dig(:spec, :domain),
82
+ load_balancer: template.dig(:spec, :load_balancer)
83
+ )
84
+ end
85
+
86
+ def identity_config_params
87
+ template.slice(:name, :description, :tags).merge(gvc: gvc)
88
+ end
89
+
90
+ def secret_config_params
91
+ template.slice(:name, :description, :type, :data, :tags)
92
+ end
93
+
94
+ def policy_config_params
95
+ template
96
+ .slice(:name, :description, :tags, :target, :target_kind, :target_query)
97
+ .merge(gvc: gvc, target_links: policy_target_links, bindings: policy_bindings)
98
+ end
99
+
100
+ def volumeset_config_params
101
+ specs = %i[
102
+ initial_capacity
103
+ performance_class
104
+ file_system_type
105
+ storage_class_suffix
106
+ snapshots
107
+ autoscaling
108
+ ].to_h { |key| [key, template.dig(:spec, key)] }
109
+
110
+ template.slice(:name, :description, :tags).merge(gvc: gvc).merge(specs)
111
+ end
112
+
113
+ def auditctx_config_params
114
+ template.slice(:name, :description, :tags)
115
+ end
116
+
117
+ def agent_config_params
118
+ template.slice(:name, :description, :tags)
119
+ end
120
+
121
+ def workload_config_params
122
+ template
123
+ .slice(:name, :description, :tags)
124
+ .merge(gvc: gvc)
125
+ .merge(workload_spec_params)
126
+ end
127
+
128
+ def workload_spec_params # rubocop:disable Metrics/MethodLength
129
+ WORKLOAD_SPEC_KEYS.to_h do |key|
130
+ arg_name =
131
+ case key
132
+ when :default_options then :options
133
+ when :firewall_config then :firewall_spec
134
+ else key
135
+ end
136
+
137
+ value = template.dig(:spec, key)
138
+
139
+ if value
140
+ case key
141
+ when :local_options
142
+ value[:location] = value.delete(:location).split("/").last
143
+ when :security_options
144
+ value[:file_system_group_id] = value.delete(:filesystem_group_id)
145
+ end
146
+ end
147
+
148
+ [arg_name, value]
149
+ end
150
+ end
151
+
152
+ # GVC name matches application name
153
+ def gvc
154
+ "cpln_gvc.#{config.app}.name"
155
+ end
156
+
157
+ def gvc_pull_secrets
158
+ template.dig(:spec, :pull_secret_links)&.map { |secret_link| "cpln_secret.#{secret_link.split('/').last}.name" }
159
+ end
160
+
161
+ def gvc_env
162
+ template.dig(:spec, :env).to_h { |env_var| [env_var[:name], env_var[:value]] }
163
+ end
164
+
165
+ def gvc_locations
166
+ template.dig(:spec, :static_placement, :location_links)&.map { |location_link| location_link.split("/").last }
167
+ end
168
+
169
+ def policy_target_links
170
+ template[:target_links]&.map { |target_link| target_link.split("/").last }
171
+ end
172
+
173
+ def policy_bindings
174
+ template[:bindings]&.map do |data|
175
+ principal_links = data.delete(:principal_links)&.map { |link| link.delete_prefix("//") }
176
+ data.merge(principal_links: principal_links)
177
+ end
178
+ end
179
+
180
+ def kind
181
+ @kind ||= template[:kind]
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformConfig
4
+ class Gvc < Base
5
+ attr_reader :name, :description, :tags, :domain, :locations, :pull_secrets, :env, :load_balancer
6
+
7
+ def initialize( # rubocop:disable Metrics/ParameterLists
8
+ name:,
9
+ description: nil,
10
+ tags: nil,
11
+ domain: nil,
12
+ locations: nil,
13
+ pull_secrets: nil,
14
+ env: nil,
15
+ load_balancer: nil
16
+ )
17
+ super()
18
+
19
+ @name = name
20
+ @description = description
21
+ @tags = tags
22
+ @domain = domain
23
+ @locations = locations
24
+ @pull_secrets = pull_secrets
25
+ @env = env
26
+ @load_balancer = load_balancer&.deep_underscore_keys&.deep_symbolize_keys
27
+ end
28
+
29
+ def importable?
30
+ true
31
+ end
32
+
33
+ def reference
34
+ "cpln_gvc.#{name}"
35
+ end
36
+
37
+ def to_tf
38
+ block :resource, :cpln_gvc, name do
39
+ argument :name, name
40
+ argument :description, description, optional: true
41
+ argument :tags, tags, optional: true
42
+
43
+ argument :domain, domain, optional: true
44
+ argument :locations, locations, optional: true
45
+ argument :pull_secrets, pull_secrets, optional: true
46
+ argument :env, env, optional: true
47
+
48
+ load_balancer_tf
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def load_balancer_tf
55
+ return if load_balancer.nil?
56
+
57
+ block :load_balancer do
58
+ argument :dedicated, load_balancer.fetch(:dedicated)
59
+ argument :trusted_proxies, load_balancer.fetch(:trusted_proxies, nil), optional: true
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformConfig
4
+ class Identity < Base
5
+ attr_reader :gvc, :name, :description, :tags
6
+
7
+ def initialize(gvc:, name:, description: nil, tags: nil)
8
+ super()
9
+
10
+ @gvc = gvc
11
+ @name = name
12
+ @description = description
13
+ @tags = tags
14
+ end
15
+
16
+ def importable?
17
+ true
18
+ end
19
+
20
+ def reference
21
+ "cpln_identity.#{name}"
22
+ end
23
+
24
+ def to_tf
25
+ block :resource, :cpln_identity, name do
26
+ argument :gvc, gvc
27
+
28
+ argument :name, name
29
+ argument :description, description, optional: true
30
+
31
+ argument :tags, tags, optional: true
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformConfig
4
+ class LocalVariable < Base
5
+ VARIABLE_NAME_REGEX = /\A[a-zA-Z][a-zA-Z0-9_]*\z/.freeze
6
+
7
+ attr_reader :variables
8
+
9
+ def initialize(**variables)
10
+ super()
11
+
12
+ @variables = variables
13
+ validate_variables!
14
+ end
15
+
16
+ def to_tf
17
+ block :locals do
18
+ variables.each do |var, value|
19
+ argument var, value
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def validate_variables!
27
+ raise ArgumentError, "Variables cannot be empty" if variables.empty?
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformConfig
4
+ class Policy < Base # rubocop:disable Metrics/ClassLength
5
+ TARGET_KINDS = %w[
6
+ agent auditctx cloudaccount domain group gvc identity image ipset kubernetes location
7
+ org policy quota secret serviceaccount task user volumeset workload
8
+ ].freeze
9
+
10
+ GVC_REQUIRED_TARGET_KINDS = %w[identity workload volumeset].freeze
11
+
12
+ attr_reader :name, :description, :tags, :target_kind, :gvc, :target, :target_links, :target_query, :bindings
13
+
14
+ def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
15
+ name:,
16
+ description: nil,
17
+ tags: nil,
18
+ target_kind: nil,
19
+ gvc: nil,
20
+ target: nil,
21
+ target_links: nil,
22
+ target_query: nil,
23
+ bindings: nil
24
+ )
25
+ super()
26
+
27
+ @name = name
28
+ @description = description
29
+ @tags = tags
30
+
31
+ @target_kind = target_kind
32
+ validate_target_kind!
33
+
34
+ @gvc = gvc
35
+ validate_gvc!
36
+
37
+ @target = target
38
+ @target_links = target_links
39
+
40
+ @target_query = target_query&.deep_underscore_keys&.deep_symbolize_keys
41
+ @bindings = bindings&.map { |data| data.deep_underscore_keys.deep_symbolize_keys }
42
+ end
43
+
44
+ def importable?
45
+ true
46
+ end
47
+
48
+ def reference
49
+ "cpln_policy.#{name}"
50
+ end
51
+
52
+ def to_tf
53
+ block :resource, :cpln_policy, name do
54
+ argument :name, name
55
+
56
+ %i[description tags target_kind gvc target target_links].each do |arg_name|
57
+ argument arg_name, send(arg_name), optional: true
58
+ end
59
+
60
+ bindings_tf
61
+ target_query_tf
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def validate_target_kind!
68
+ return if target_kind.nil? || TARGET_KINDS.include?(target_kind.to_s)
69
+
70
+ raise ArgumentError, "Invalid target kind given - #{target_kind}"
71
+ end
72
+
73
+ def validate_gvc!
74
+ return unless GVC_REQUIRED_TARGET_KINDS.include?(target_kind.to_s) && gvc.nil?
75
+
76
+ raise ArgumentError, "`gvc` is required for `#{target_kind}` target kind"
77
+ end
78
+
79
+ def bindings_tf
80
+ return if bindings.nil?
81
+
82
+ bindings.each do |binding_data|
83
+ block :binding do
84
+ argument :permissions, binding_data.fetch(:permissions, nil), optional: true
85
+ argument :principal_links, binding_data.fetch(:principal_links, nil), optional: true
86
+ end
87
+ end
88
+ end
89
+
90
+ def target_query_tf
91
+ return if target_query.nil?
92
+
93
+ fetch_type = target_query.fetch(:fetch, nil)
94
+ validate_fetch_type!(fetch_type) if fetch_type
95
+
96
+ block :target_query do
97
+ argument :fetch, fetch_type, optional: true
98
+ target_query_spec_tf
99
+ end
100
+ end
101
+
102
+ def validate_fetch_type!(fetch_type)
103
+ return if %w[links items].include?(fetch_type.to_s)
104
+
105
+ raise ArgumentError, "Invalid fetch type - #{fetch_type}. Should be either `links` or `items`"
106
+ end
107
+
108
+ def target_query_spec_tf
109
+ spec = target_query.fetch(:spec, nil)
110
+ return if spec.nil?
111
+
112
+ match_type = spec.fetch(:match, nil)
113
+ validate_match_type!(match_type) if match_type
114
+
115
+ block :spec do
116
+ argument :match, match_type, optional: true
117
+
118
+ target_query_spec_terms_tf(spec)
119
+ end
120
+ end
121
+
122
+ def validate_match_type!(match_type)
123
+ return if %w[all any none].include?(match_type.to_s)
124
+
125
+ raise ArgumentError, "Invalid match type - #{match_type}. Should be either `all`, `any` or `none`"
126
+ end
127
+
128
+ def target_query_spec_terms_tf(spec)
129
+ terms = spec.fetch(:terms, nil)
130
+ return if terms.nil?
131
+
132
+ terms.each do |term|
133
+ validate_term!(term)
134
+
135
+ block :terms do
136
+ %i[op property rel tag value].each do |arg_name|
137
+ argument arg_name, term.fetch(arg_name, nil), optional: true
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ def validate_term!(term)
144
+ return if (%i[property rel tag] & term.keys).count == 1
145
+
146
+ raise ArgumentError,
147
+ "Each term in `target_query.spec.terms` must contain exactly one of the following attributes: " \
148
+ "`property`, `rel`, or `tag`."
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformConfig
4
+ class Provider < Base
5
+ attr_reader :name, :options
6
+
7
+ def initialize(name:, **options)
8
+ super()
9
+
10
+ @name = name
11
+ @options = options
12
+ end
13
+
14
+ def to_tf
15
+ block :provider, name do
16
+ options.each do |option, value|
17
+ argument option, value
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformConfig
4
+ class RequiredProvider < Base
5
+ attr_reader :name, :org, :options
6
+
7
+ def initialize(name:, org:, **options)
8
+ super()
9
+
10
+ @name = name
11
+ @org = org
12
+ @options = options
13
+ end
14
+
15
+ def to_tf
16
+ block :terraform do
17
+ block :required_providers do
18
+ argument name, options
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformConfig
4
+ class Secret < Base # rubocop:disable Metrics/ClassLength
5
+ REQUIRED_DATA_KEYS = {
6
+ "aws" => %i[secret_key access_key],
7
+ "azure-connector" => %i[url code],
8
+ "ecr" => %i[secret_key access_key repos],
9
+ "keypair" => %i[secret_key],
10
+ "nats-account" => %i[account_id private_key],
11
+ "opaque" => %i[payload],
12
+ "tls" => %i[key cert],
13
+ "userpass" => %i[username password],
14
+ "dictionary" => []
15
+ }.freeze
16
+
17
+ attr_reader :name, :type, :data, :description, :tags
18
+
19
+ def initialize(name:, type:, data:, description: nil, tags: nil)
20
+ super()
21
+
22
+ @name = name
23
+ @type = type
24
+ @description = description
25
+ @tags = tags
26
+ @data = prepare_data(type: type, data: data)
27
+ end
28
+
29
+ def importable?
30
+ true
31
+ end
32
+
33
+ def reference
34
+ "cpln_secret.#{name}"
35
+ end
36
+
37
+ def to_tf
38
+ block :resource, :cpln_secret, name do
39
+ argument :name, name
40
+ argument :description, description, optional: true
41
+ argument :tags, tags, optional: true
42
+
43
+ secret_data
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def prepare_data(type:, data:)
50
+ return data unless data.is_a?(Hash)
51
+
52
+ data.deep_underscore_keys.deep_symbolize_keys.tap do |prepared_data|
53
+ validate_required_data_keys!(type: type, data: prepared_data)
54
+ end
55
+ end
56
+
57
+ def validate_required_data_keys!(type:, data:)
58
+ required_keys = REQUIRED_DATA_KEYS[type] || []
59
+ missing_keys = required_keys - data.keys
60
+ raise ArgumentError, "Missing required data keys for #{type}: #{missing_keys.join(', ')}" if missing_keys.any?
61
+ end
62
+
63
+ def secret_data
64
+ case type
65
+ when "azure-sdk", "dictionary", "docker", "gcp"
66
+ argument type.underscore, data, optional: true
67
+ when "azure-connector", "aws", "ecr", "keypair", "nats-account", "opaque", "tls", "userpass"
68
+ send("#{type.underscore}_tf")
69
+ else
70
+ raise "Invalid secret type given - #{type}"
71
+ end
72
+ end
73
+
74
+ def aws_tf
75
+ aws_based_tf(:aws)
76
+ end
77
+
78
+ def ecr_tf
79
+ aws_based_tf(:ecr, repos: data.fetch(:repos))
80
+ end
81
+
82
+ def azure_connector_tf
83
+ block :azure_connector do
84
+ argument :url, data.fetch(:url)
85
+ argument :code, data.fetch(:code)
86
+ end
87
+ end
88
+
89
+ def keypair_tf
90
+ block :keypair do
91
+ argument :secret_key, data.fetch(:secret_key)
92
+ argument :public_key, data.fetch(:public_key, nil), optional: true
93
+ argument :passphrase, data.fetch(:passphrase, nil), optional: true
94
+ end
95
+ end
96
+
97
+ def nats_account_tf
98
+ block :nats_account do
99
+ argument :account_id, data.fetch(:account_id)
100
+ argument :private_key, data.fetch(:private_key)
101
+ end
102
+ end
103
+
104
+ def opaque_tf
105
+ block :opaque do
106
+ argument :payload, data.fetch(:payload)
107
+ argument :encoding, data.fetch(:encoding, nil), optional: true
108
+ end
109
+ end
110
+
111
+ def tls_tf
112
+ block :tls do
113
+ argument :key, data.fetch(:key)
114
+ argument :cert, data.fetch(:cert)
115
+ argument :chain, data.fetch(:chain, nil), optional: true
116
+ end
117
+ end
118
+
119
+ def userpass_tf
120
+ block :userpass do
121
+ argument :username, data.fetch(:username)
122
+ argument :password, data.fetch(:password)
123
+ argument :encoding, data.fetch(:encoding, nil), optional: true
124
+ end
125
+ end
126
+
127
+ def aws_based_tf(name, **kwargs)
128
+ block name do
129
+ argument :secret_key, data.fetch(:secret_key)
130
+ argument :access_key, data.fetch(:access_key)
131
+ argument :role_arn, data.fetch(:role_arn, nil), optional: true
132
+ argument :external_id, data.fetch(:external_id, nil), optional: true
133
+
134
+ kwargs.each { |key, value| argument key, value }
135
+ end
136
+ end
137
+ end
138
+ end