Skip to content

Commit

Permalink
Generate Terraform config from policy templates
Browse files Browse the repository at this point in the history
  • Loading branch information
zzaakiirr committed Oct 16, 2024
1 parent a58bcf9 commit 4e391a2
Show file tree
Hide file tree
Showing 12 changed files with 480 additions and 32 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 @@ -47,7 +47,7 @@ def generate_app_config(app)
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
52 changes: 41 additions & 11 deletions lib/core/terraform_config/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ def initialize(config:, template:)
@template = template
end

# rubocop:disable Metrics/MethodLength
def filename
case template["kind"]
when "gvc"
Expand All @@ -17,22 +18,19 @@ def filename
"secrets.tf"
when "identity"
"identities.tf"
when "policy"
"policies.tf"
else
raise "Unsupported template kind - #{template['kind']}"
end
end
# rubocop:enable Metrics/MethodLength

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 = :"#{template['kind']}_config"
raise "Unsupported template kind - #{template['kind']}" unless self.class.private_method_defined?(method_name)

send(method_name)
end

private
Expand Down Expand Up @@ -61,7 +59,7 @@ def gvc_config

def identity_config
TerraformConfig::Identity.new(
gvc: "cpln_gvc.#{config.app}.name", # GVC name matches application name
gvc: gvc,
name: template["name"],
description: template["description"],
tags: template["tags"]
Expand All @@ -78,6 +76,38 @@ def secret_config
)
end

# rubocop:disable Metrics/MethodLength
def policy_config
# //secret/secret-name -> secret-name
target_links = template["targetLinks"]&.map do |target_link|
target_link.split("/").last
end

# //group/viewers -> group/viewers
bindings = template["bindings"]&.map do |data|
principal_links = data.delete("principalLinks")&.map { |link| link.delete_prefix("//") }
data.merge("principalLinks" => principal_links)
end

TerraformConfig::Policy.new(
name: template["name"],
description: template["description"],
tags: template["tags"],
target: template["target"],
target_kind: template["targetKind"],
target_query: template["targetQuery"],
target_links: target_links,
gvc: gvc,
bindings: bindings
)
end
# rubocop:enable Metrics/MethodLength

# GVC name matches application name
def gvc
"cpln_gvc.#{config.app}.name"
end

def env
template.dig("spec", "env").to_h { |env_var| [env_var["name"], env_var["value"]] }
end
Expand Down
2 changes: 1 addition & 1 deletion lib/core/terraform_config/gvc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def initialize(
@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
# rubocop:enable Metrics/ParameterLists

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

module TerraformConfig
# rubocop:disable Metrics/ClassLength
class Policy < Base
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_REQUIERD_TARGET_KINDS = %w[identity workload volumeset].freeze

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

# rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
def initialize(
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
# rubocop:enable Metrics/ParameterLists, Metrics/MethodLength

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_REQUIERD_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,
"`target_query.spec.terms` can contain only one of the following attributes: `property`, `rel`, `tag`."
end
end
# rubocop:enable Metrics/ClassLength
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 @@ -42,7 +42,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 @@ -99,7 +99,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
52 changes: 52 additions & 0 deletions spec/core/terraform_config/generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,56 @@
expect(tf_filename).to eq("secrets.tf")
end
end

context "when template's kind is policy" do
let(:template) do
{
"kind" => "policy",
"name" => "policy-name",
"description" => "policy description",
"tags" => { "tag1" => "tag1_value", "tag2" => "tag2_value" },
"target" => "all",
"targetKind" => "secret",
"targetLinks" => [
"//secret/postgres-poc-credentials",
"//secret/postgres-poc-entrypoint-script"
],
"bindings" => [
{
"permissions" => %w[reveal view use],
"principalLinks" => %W[//gvc/#{config.app}/identity/postgres-poc-identity]
},
{
"permissions" => %w[view],
"principalLinks" => %w[user/fake-user@fake-email.com]
}
]
}
end

it "generates correct terraform config and filename for it", :aggregate_failures do
tf_config = generator.tf_config
expect(tf_config).to be_an_instance_of(TerraformConfig::Policy)

expect(tf_config.name).to eq("policy-name")
expect(tf_config.description).to eq("policy description")
expect(tf_config.tags).to eq("tag1" => "tag1_value", "tag2" => "tag2_value")
expect(tf_config.target).to eq("all")
expect(tf_config.target_kind).to eq("secret")
expect(tf_config.target_links).to eq(%w[postgres-poc-credentials postgres-poc-entrypoint-script])
expect(tf_config.bindings).to contain_exactly(
{
permissions: %w[reveal view use],
principal_links: %W[gvc/#{config.app}/identity/postgres-poc-identity]
},
{
permissions: %w[view],
principal_links: %w[user/fake-user@fake-email.com]
}
)

tf_filename = generator.filename
expect(tf_filename).to eq("policies.tf")
end
end
end
Loading

0 comments on commit 4e391a2

Please sign in to comment.