Skip to content

Commit

Permalink
Support terraform config generation from secret template (#235)
Browse files Browse the repository at this point in the history
  • Loading branch information
zzaakiirr authored Oct 21, 2024
1 parent b4bf309 commit fa966c8
Show file tree
Hide file tree
Showing 12 changed files with 698 additions and 11 deletions.
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].include?(template["kind"])
next unless %w[gvc identity secret].include?(template["kind"])

File.write(terraform_app_dir.join(generator.filename), generator.tf_config.to_tf, mode: "a+")
end
Expand Down
13 changes: 6 additions & 7 deletions lib/core/terraform_config/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,14 @@ def argument(name, value, optional: false)

private

def tf_value(value)
def tf_value(value, heredoc_delimiter: "EOF", multiline_indent: 2)
value = value.to_s if value.is_a?(Symbol)

case value
when String
expression?(value) ? value : "\"#{value}\""
else
value
end
return value unless value.is_a?(String)
return value if expression?(value)
return "\"#{value}\"" unless value.include?("\n")

"#{heredoc_delimiter}\n#{value.indent(multiline_indent)}\n#{heredoc_delimiter}"
end

def expression?(value)
Expand Down
14 changes: 14 additions & 0 deletions lib/core/terraform_config/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ def filename
"gvc.tf"
when "identity"
"identities.tf"
when "secret"
"secrets.tf"
else
raise "Unsupported template kind - #{template['kind']}"
end
Expand All @@ -26,6 +28,8 @@ def tf_config
gvc_config
when "identity"
identity_config
when "secret"
secret_config
else
raise "Unsupported template kind - #{template['kind']}"
end
Expand Down Expand Up @@ -62,6 +66,16 @@ def identity_config
)
end

def secret_config
TerraformConfig::Secret.new(
name: template["name"],
description: template["description"],
type: template["type"],
data: template["data"],
tags: template["tags"]
)
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 @@ -23,7 +23,7 @@ def initialize( # rubocop:disable Metrics/ParameterLists
@locations = locations
@pull_secrets = pull_secrets
@env = env
@load_balancer = load_balancer&.transform_keys { |k| k.to_s.underscore.to_sym }
@load_balancer = load_balancer&.underscore_keys&.symbolize_keys
end

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

module TerraformConfig
class Secret < Base # rubocop:disable Metrics/ClassLength
REQUIRED_DATA_KEYS = {
"aws" => %i[secret_key access_key],
"azure-connector" => %i[url code],
"ecr" => %i[secret_key access_key repos],
"keypair" => %i[secret_key],
"nats-account" => %i[account_id private_key],
"opaque" => %i[payload],
"tls" => %i[key cert],
"userpass" => %i[username password],
"dictionary" => []
}.freeze

attr_reader :name, :type, :data, :description, :tags

def initialize(name:, type:, data:, description: nil, tags: nil)
super()

@name = name
@type = type
@description = description
@tags = tags
@data = prepare_data(type: type, data: data)
end

def to_tf
block :resource, :cpln_secret, name do
argument :name, name
argument :description, description, optional: true
argument :tags, tags, optional: true

secret_data
end
end

private

def prepare_data(type:, data:)
return data unless data.is_a?(Hash)

data.underscore_keys.symbolize_keys.tap do |prepared_data|
validate_required_data_keys!(type: type, data: prepared_data)
end
end

def validate_required_data_keys!(type:, data:)
required = REQUIRED_DATA_KEYS[type]
missing_keys = required - data.keys
raise ArgumentError, "Missing required data keys for #{type}: #{missing_keys.join(', ')}" if missing_keys.any?
end

def secret_data
case type
when "azure-sdk", "dictionary", "docker", "gcp"
argument type.underscore, data, optional: true
when "azure-connector", "aws", "ecr", "keypair", "nats-account", "opaque", "tls", "userpass"
send("#{type.underscore}_tf")
else
raise "Invalid secret type given - #{type}"
end
end

def aws_tf
aws_based_tf(:aws)
end

def ecr_tf
aws_based_tf(:ecr, repos: data.fetch(:repos))
end

def azure_connector_tf
block :azure_connector do
argument :url, data.fetch(:url)
argument :code, data.fetch(:code)
end
end

def keypair_tf
block :keypair do
argument :secret_key, data.fetch(:secret_key)
argument :public_key, data.fetch(:public_key, nil), optional: true
argument :passphrase, data.fetch(:passphrase, nil), optional: true
end
end

def nats_account_tf
block :nats_account do
argument :account_id, data.fetch(:account_id)
argument :private_key, data.fetch(:private_key)
end
end

def opaque_tf
block :opaque do
argument :payload, data.fetch(:payload)
argument :encoding, data.fetch(:encoding, nil), optional: true
end
end

def tls_tf
block :tls do
argument :key, data.fetch(:key)
argument :cert, data.fetch(:cert)
argument :chain, data.fetch(:chain, nil), optional: true
end
end

def userpass_tf
block :userpass do
argument :username, data.fetch(:username)
argument :password, data.fetch(:password)
argument :encoding, data.fetch(:encoding, nil), optional: true
end
end

def aws_based_tf(name, **kwargs)
block name do
argument :secret_key, data.fetch(:secret_key)
argument :access_key, data.fetch(:access_key)
argument :role_arn, data.fetch(:role_arn, nil), optional: true
argument :external_id, data.fetch(:external_id, nil), optional: true

kwargs.each { |key, value| argument key, value }
end
end
end
end
17 changes: 17 additions & 0 deletions lib/patches/hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

class Hash
# Copied from Rails
def symbolize_keys
transform_keys { |key| key.to_sym rescue key } # rubocop:disable Style/RescueModifier
end

def underscore_keys
transform_keys do |key|
underscored = key.to_s.underscore
key.is_a?(Symbol) ? underscored.to_sym : underscored
rescue StandardError
key
end
end
end
3 changes: 2 additions & 1 deletion lib/patches/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ def unindent
gsub(/^#{scan(/^[ \t]+(?=\S)/).min}/, "")
end

# Copied from Rails
def underscore
gsub(/(.)([A-Z])/, '\1_\2').downcase
gsub("::", "/").gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').tr("-", "_").downcase
end
end
# rubocop:enable Style/OptionalBooleanParameter, Lint/UnderscorePrefixedVariableName
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].map do |config_file_path|
%w[gvc.tf identities.tf secrets.tf].map do |config_file_path|
TERRAFORM_CONFIG_DIR_PATH.join(app, config_file_path)
end
end
Expand Down
34 changes: 34 additions & 0 deletions spec/core/terraform_config/generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@

let(:config) { instance_double(Config, org: "org-name", app: "app-name") }

context "when template's kind is unsupported" do
let(:template) { { "kind" => "invalid" } }

it "does not generate terraform config or filename for it", :aggregate_failures do
expect { generator.tf_config }.to raise_error("Unsupported template kind - #{template['kind']}")
expect { generator.filename }.to raise_error("Unsupported template kind - #{template['kind']}")
end
end

context "when template's kind is gvc" do
let(:template) do
{
Expand Down Expand Up @@ -89,4 +98,29 @@
expect(tf_filename).to eq("identities.tf")
end
end

context "when template's kind is secret" do
let(:template) do
{
"kind" => "secret",
"type" => "dictionary",
"name" => "secret-name",
"description" => "description",
"tags" => { "tag1" => "tag1_value", "tag2" => "tag2_value" },
"data" => { "key1" => "key1_value", "key2" => "key2_value2" }
}
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::Secret)

expect(tf_config.name).to eq("secret-name")
expect(tf_config.description).to eq("description")
expect(tf_config.tags).to eq("tag1" => "tag1_value", "tag2" => "tag2_value")

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

0 comments on commit fa966c8

Please sign in to comment.