Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate Terraform config from policy templates #236

4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ RSpec/ExampleLength:

RSpec/MultipleExpectations:
Enabled: false

RSpec/NestedGroups:
Enabled: true
Max: 5
2 changes: 1 addition & 1 deletion lib/command/terraform/generate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def generate_app_config
generator = TerraformConfig::Generator.new(config: config, template: template)

# TODO: Delete line below after all template kinds are supported
next unless %w[gvc identity secret].include?(template["kind"])
next unless %w[gvc identity secret policy].include?(template["kind"])

File.write(terraform_app_dir.join(generator.filename), generator.tf_config.to_tf, mode: "a+")
end
Expand Down
112 changes: 65 additions & 47 deletions lib/core/terraform_config/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,84 +6,102 @@ class Generator

def initialize(config:, template:)
@config = config
@template = template
@template = template.deep_underscore_keys.deep_symbolize_keys
end

def filename
case template["kind"]
def filename # rubocop:disable Metrics/MethodLength
case kind
when "gvc"
"gvc.tf"
when "identity"
"identities.tf"
when "secret"
"secrets.tf"
when "identity"
"identities.tf"
when "policy"
"policies.tf"
else
raise "Unsupported template kind - #{template['kind']}"
raise "Unsupported template kind - #{kind}"
end
end

def tf_config
case template["kind"]
when "gvc"
gvc_config
when "identity"
identity_config
when "secret"
secret_config
else
raise "Unsupported template kind - #{template['kind']}"
end
method_name = :"#{kind}_config"
raise "Unsupported template kind - #{kind}" unless self.class.private_method_defined?(method_name)

send(method_name)
end

private

def gvc_config # rubocop:disable Metrics/MethodLength
pull_secrets = template.dig("spec", "pullSecretLinks")&.map do |secret_link|
secret_name = secret_link.split("/").last
"cpln_secret.#{secret_name}.name"
end

load_balancer = template.dig("spec", "loadBalancer")
def kind
@kind ||= template[:kind]
end

def gvc_config # rubocop:disable Metrics/MethodLength
TerraformConfig::Gvc.new(
name: template["name"],
description: template["description"],
tags: template["tags"],
domain: template.dig("spec", "domain"),
env: env,
pull_secrets: pull_secrets,
locations: locations,
load_balancer: load_balancer
**template
.slice(:name, :description, :tags)
.merge(
env: gvc_env,
pull_secrets: gvc_pull_secrets,
locations: gvc_locations,
domain: template.dig(:spec, :domain),
load_balancer: template.dig(:spec, :load_balancer)
)
)
end

def identity_config
TerraformConfig::Identity.new(
gvc: "cpln_gvc.#{config.app}.name", # GVC name matches application name
name: template["name"],
description: template["description"],
tags: template["tags"]
)
TerraformConfig::Identity.new(**template.slice(:name, :description, :tags).merge(gvc: gvc))
end

def secret_config
TerraformConfig::Secret.new(
name: template["name"],
description: template["description"],
type: template["type"],
data: template["data"],
tags: template["tags"]
TerraformConfig::Secret.new(**template.slice(:name, :description, :type, :data, :tags))
end

def policy_config
TerraformConfig::Policy.new(
**template
.slice(:name, :description, :tags, :target, :target_kind, :target_query)
.merge(gvc: gvc, target_links: policy_target_links, bindings: policy_bindings)
)
end

def env
template.dig("spec", "env").to_h { |env_var| [env_var["name"], env_var["value"]] }
# GVC name matches application name
def gvc
"cpln_gvc.#{config.app}.name"
end

def gvc_pull_secrets
template.dig(:spec, :pull_secret_links)&.map do |secret_link|
secret_name = secret_link.split("/").last
"cpln_secret.#{secret_name}.name"
end
end

def gvc_env
template.dig(:spec, :env).to_h { |env_var| [env_var[:name], env_var[:value]] }
end

def locations
template.dig("spec", "staticPlacement", "locationLinks")&.map do |location_link|
def gvc_locations
template.dig(:spec, :static_placement, :location_links)&.map do |location_link|
location_link.split("/").last
end
end

# //secret/secret-name -> secret-name
def policy_target_links
template[:target_links]&.map do |target_link|
target_link.split("/").last
end
end

# //group/viewers -> group/viewers
def policy_bindings
template[:bindings]&.map do |data|
principal_links = data.delete(:principal_links)&.map { |link| link.delete_prefix("//") }
data.merge(principal_links: principal_links)
end
end
end
end
2 changes: 1 addition & 1 deletion lib/core/terraform_config/gvc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def initialize( # rubocop:disable Metrics/ParameterLists
@locations = locations
@pull_secrets = pull_secrets
@env = env
@load_balancer = load_balancer&.underscore_keys&.symbolize_keys
@load_balancer = load_balancer&.deep_underscore_keys&.deep_symbolize_keys
end

def to_tf
Expand Down
143 changes: 143 additions & 0 deletions lib/core/terraform_config/policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# frozen_string_literal: true

module TerraformConfig
class Policy < Base # rubocop:disable Metrics/ClassLength
TARGET_KINDS = %w[
agent auditctx cloudaccount domain group gvc identity image ipset kubernetes location
org policy quota secret serviceaccount task user volumeset workload
].freeze

GVC_REQUIRED_TARGET_KINDS = %w[identity workload volumeset].freeze

attr_reader :name, :description, :tags, :target_kind, :gvc, :target, :target_links, :target_query, :bindings

def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
name:,
description: nil,
tags: nil,
target_kind: nil,
gvc: nil,
target: nil,
target_links: nil,
target_query: nil,
bindings: nil
)
super()

@name = name
@description = description
@tags = tags

@target_kind = target_kind
validate_target_kind!

@gvc = gvc
validate_gvc!

@target = target
@target_links = target_links

@target_query = target_query&.deep_underscore_keys&.deep_symbolize_keys
@bindings = bindings&.map { |data| data.deep_underscore_keys.deep_symbolize_keys }
end

def to_tf
block :resource, :cpln_policy, name do
argument :name, name

%i[description tags target_kind gvc target target_links].each do |arg_name|
argument arg_name, send(arg_name), optional: true
end

bindings_tf
target_query_tf
end
end

private

def validate_target_kind!
return if target_kind.nil? || TARGET_KINDS.include?(target_kind.to_s)

raise ArgumentError, "Invalid target kind given - #{target_kind}"
end

def validate_gvc!
return unless GVC_REQUIRED_TARGET_KINDS.include?(target_kind.to_s) && gvc.nil?

raise ArgumentError, "`gvc` is required for `#{target_kind}` target kind"
end

def bindings_tf
return if bindings.nil?

bindings.each do |binding_data|
block :binding do
argument :permissions, binding_data.fetch(:permissions, nil), optional: true
argument :principal_links, binding_data.fetch(:principal_links, nil), optional: true
end
end
end

def target_query_tf
return if target_query.nil?

fetch_type = target_query.fetch(:fetch, nil)
validate_fetch_type!(fetch_type) if fetch_type

block :target_query do
argument :fetch, fetch_type, optional: true
target_query_spec_tf
end
end

def validate_fetch_type!(fetch_type)
return if %w[links items].include?(fetch_type.to_s)

raise ArgumentError, "Invalid fetch type - #{fetch_type}. Should be either `links` or `items`"
end

def target_query_spec_tf
spec = target_query.fetch(:spec, nil)
return if spec.nil?

match_type = spec.fetch(:match, nil)
validate_match_type!(match_type) if match_type

block :spec do
argument :match, match_type, optional: true

target_query_spec_terms_tf(spec)
end
end

def validate_match_type!(match_type)
return if %w[all any none].include?(match_type.to_s)

raise ArgumentError, "Invalid match type - #{match_type}. Should be either `all`, `any` or `none`"
end

def target_query_spec_terms_tf(spec)
terms = spec.fetch(:terms, nil)
return if terms.nil?

terms.each do |term|
validate_term!(term)

block :terms do
%i[op property rel tag value].each do |arg_name|
argument arg_name, term.fetch(arg_name, nil), optional: true
end
end
end
end

def validate_term!(term)
return unless (%i[property rel tag] & term.keys).count > 1

raise ArgumentError,
"Each term in `target_query.spec.terms` must contain exactly one of the following attributes: " \
"`property`, `rel`, or `tag`."
end
end
end
2 changes: 1 addition & 1 deletion lib/core/terraform_config/secret.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def to_tf
def prepare_data(type:, data:)
return data unless data.is_a?(Hash)

data.underscore_keys.symbolize_keys.tap do |prepared_data|
data.deep_underscore_keys.deep_symbolize_keys.tap do |prepared_data|
validate_required_data_keys!(type: type, data: prepared_data)
end
end
Expand Down
29 changes: 25 additions & 4 deletions lib/patches/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,37 @@

class Hash
# Copied from Rails
def symbolize_keys
transform_keys { |key| key.to_sym rescue key } # rubocop:disable Style/RescueModifier
def deep_symbolize_keys
deep_transform_keys { |key| key.to_sym rescue key } # rubocop:disable Style/RescueModifier
zzaakiirr marked this conversation as resolved.
Show resolved Hide resolved
end

def underscore_keys
transform_keys do |key|
def deep_underscore_keys
deep_transform_keys do |key|
underscored = key.to_s.underscore
key.is_a?(Symbol) ? underscored.to_sym : underscored
rescue StandardError
key
end
end
zzaakiirr marked this conversation as resolved.
Show resolved Hide resolved

private

# Copied from Rails
def deep_transform_keys(&block)
deep_transform_keys_in_object(self, &block)
end

# Copied from Rails
def deep_transform_keys_in_object(object, &block)
case object
when Hash
object.each_with_object(self.class.new) do |(key, value), result|
result[yield(key)] = deep_transform_keys_in_object(value, &block)
end
when Array
object.map { |e| deep_transform_keys_in_object(e, &block) }
else
object
end
end
end
2 changes: 1 addition & 1 deletion spec/command/terraform/generate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def common_config_files
end

def app_config_files
%w[gvc.tf identities.tf secrets.tf].map do |config_file_path|
%w[gvc.tf identities.tf secrets.tf policies.tf].map do |config_file_path|
TERRAFORM_CONFIG_DIR_PATH.join(app, config_file_path)
end
end
Expand Down
Loading
Loading