Skip to content

Commit

Permalink
Generate Terraform config from policy templates (#236)
Browse files Browse the repository at this point in the history
  • Loading branch information
zzaakiirr authored Oct 21, 2024
1 parent fa966c8 commit 40aba4c
Show file tree
Hide file tree
Showing 12 changed files with 505 additions and 71 deletions.
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
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

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

0 comments on commit 40aba4c

Please sign in to comment.