Skip to content

Commit

Permalink
Generate terraform configs from workload templates
Browse files Browse the repository at this point in the history
  • Loading branch information
zzaakiirr committed Nov 1, 2024
1 parent 288046a commit 11ed4ff
Show file tree
Hide file tree
Showing 23 changed files with 1,759 additions and 90 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@

/spec.log
/spec/dummy/.controlplane/controlplane*-tmp-*.yml

# Generated configs
terraform/
.controlplane/
50 changes: 35 additions & 15 deletions lib/command/terraform/generate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,27 @@ def call
private

def generate_app_config
terraform_app_dir = recreate_terraform_app_dir
copy_workload_module

terraform_app_dir = cleaned_terraform_app_dir
generate_provider_configs(terraform_app_dir)

templates.each do |template|
generator = TerraformConfig::Generator.new(config: config, template: template)
File.write(terraform_app_dir.join(generator.filename), generator.tf_config.to_tf, mode: "a+")
TerraformConfig::Generator.new(config: config, template: template).tf_configs.each do |filename, tf_config|
File.write(terraform_app_dir.join(filename), tf_config.to_tf, mode: "a+")
end
rescue TerraformConfig::Generator::InvalidTemplateError => e
Shell.warn(e.message)
rescue StandardError => e
Shell.warn("Failed to generate config file from '#{template['kind']}' template: #{e.message}")
end
end

def copy_workload_module
FileUtils.copy_entry(
Cpflow.root_path.join("lib/core/terraform_config/workload"),
terraform_dir.join("workload")
)
end

def generate_provider_configs(terraform_app_dir)
generate_required_providers(terraform_app_dir)
generate_providers(terraform_app_dir)
Expand All @@ -47,13 +54,6 @@ def generate_provider_configs(terraform_app_dir)
end

def generate_required_providers(terraform_app_dir)
required_cpln_provider = TerraformConfig::RequiredProvider.new(
name: "cpln",
org: config.org,
source: "controlplane-com/cpln",
version: "~> 1.0"
)

File.write(terraform_app_dir.join("required_providers.tf"), required_cpln_provider.to_tf)
end

Expand All @@ -62,19 +62,39 @@ def generate_providers(terraform_app_dir)
File.write(terraform_app_dir.join("providers.tf"), cpln_provider.to_tf)
end

def recreate_terraform_app_dir
def required_cpln_provider
TerraformConfig::RequiredProvider.new(
name: "cpln",
org: config.org,
source: "controlplane-com/cpln",
version: "~> 1.0"
)
end

def cleaned_terraform_app_dir
full_path = terraform_dir.join(config.app)

unless File.expand_path(full_path).include?(Cpflow.root_path.to_s)
Shell.abort("Directory to save terraform configuration files cannot be outside of current directory")
end

FileUtils.rm_rf(full_path)
FileUtils.mkdir_p(full_path)
if Dir.exist?(full_path)
clean_terraform_app_dir(full_path)
else
FileUtils.mkdir_p(full_path)
end

full_path
end

def clean_terraform_app_dir(terraform_app_dir)
Dir.children(terraform_app_dir).each do |child|
next if child == ".terraform.lock.hcl"

FileUtils.rm_rf(terraform_app_dir.join(child))
end
end

def templates
parser = TemplateParser.new(self)
template_files = Dir["#{parser.template_dir}/*.yml"]
Expand Down
4 changes: 4 additions & 0 deletions lib/core/terraform_config/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ class Base
def to_tf
raise NotImplementedError
end

def locals
{}
end
end
end
31 changes: 24 additions & 7 deletions lib/core/terraform_config/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module TerraformConfig
module Dsl
extend Forwardable

REFERENCE_PATTERN = /^(var|locals|cpln_\w+)\./.freeze
EXPRESSION_PATTERN = /(var|local|cpln_\w+)\./.freeze

def_delegators :current_context, :put, :output

Expand All @@ -19,12 +19,13 @@ def block(name, *labels)
output.unindent
end

def argument(name, value, optional: false)
def argument(name, value, optional: false, raw: false)
return if value.nil? && optional

content =
if value.is_a?(Hash)
"{\n#{value.map { |n, v| "#{n} = #{tf_value(v)}" }.join("\n").indent(2)}\n}\n"
operator = raw ? ": " : " = "
"{\n#{value.map { |n, v| "#{n}#{operator}#{tf_value(v)}" }.join("\n").indent(2)}\n}\n"
else
"#{tf_value(value)}\n"
end
Expand All @@ -34,18 +35,34 @@ def argument(name, value, optional: false)

private

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

return value unless value.is_a?(String)
case value
when String
tf_string_value(value)
when Hash
tf_hash_value(value)
else
value
end
end

def tf_string_value(value)
return value if expression?(value)
return "\"#{value}\"" unless value.include?("\n")

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

def tf_hash_value(value)
JSON.pretty_generate(value.crush)
.gsub(/"(\w+)":/) { "#{::Regexp.last_match(1)}:" } # remove quotes from keys
.gsub(/("#{EXPRESSION_PATTERN}.*")/) { ::Regexp.last_match(1)[1...-1] } # remove quotes from expression values
end

def expression?(value)
value.match?(REFERENCE_PATTERN)
value.match?(/^#{EXPRESSION_PATTERN}/)
end

def block_declaration(name, labels)
Expand Down
73 changes: 62 additions & 11 deletions lib/core/terraform_config/generator.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
# frozen_string_literal: true

module TerraformConfig
class Generator
SUPPORTED_TEMPLATE_KINDS = %w[gvc secret identity policy volumeset].freeze
class Generator # rubocop:disable Metrics/ClassLength
SUPPORTED_TEMPLATE_KINDS = %w[gvc secret identity policy volumeset workload].freeze
WORKLOAD_SPEC_KEYS = %i[
type
containers
default_options
local_options
rollout_options
security_options
load_balancer
firewall_config
support_dynamic_tags
job
].freeze

class InvalidTemplateError < ArgumentError; end

Expand All @@ -14,14 +26,8 @@ def initialize(config:, template:)
validate_template_kind!
end

def filename
return "gvc.tf" if kind == "gvc"

"#{kind.pluralize}.tf"
end

def tf_config
config_class.new(**config_params)
def tf_configs
tf_config.locals.merge(filename => tf_config)
end

private
Expand All @@ -32,6 +38,21 @@ def validate_template_kind!
raise InvalidTemplateError, "Unsupported template kind: #{kind}"
end

def filename
case kind
when "gvc"
"gvc.tf"
when "workload"
"#{template[:name]}.tf"
else
"#{kind.pluralize}.tf"
end
end

def tf_config
config_class.new(**config_params)
end

def config_class
if kind == "volumeset"
TerraformConfig::VolumeSet
Expand Down Expand Up @@ -83,6 +104,29 @@ def volumeset_config_params
template.slice(:name, :description, :tags).merge(gvc: gvc).merge(specs)
end

def workload_config_params
template
.slice(:name, :description, :tags)
.merge(gvc: gvc, identity: workload_identity)
.merge(workload_spec_params)
end

def workload_spec_params # rubocop:disable Metrics/MethodLength
WORKLOAD_SPEC_KEYS.to_h do |key|
arg_name =
case key
when :default_options then :options
when :firewall_config then :firewall_spec
else key
end

value = template.dig(:spec, key)
value.merge!(location: value[:location].split("/").last) if value && key == :local_options

[arg_name, value]
end
end

# GVC name matches application name
def gvc
"cpln_gvc.#{config.app}.name"
Expand All @@ -106,11 +150,18 @@ def policy_target_links

def policy_bindings
template[:bindings]&.map do |data|
principal_links = data.delete(:principal_links)&.map { |link| link.delete_prefix("//") }
principal_links = data[:principal_links]&.map { |link| link.delete_prefix("//") }
data.merge(principal_links: principal_links)
end
end

def workload_identity
identity_link = template.dig(:spec, :identity_link)
return if identity_link.nil?

"cpln_identity.#{identity_link.split('/').last}"
end

def kind
@kind ||= template[:kind]
end
Expand Down
21 changes: 21 additions & 0 deletions lib/core/terraform_config/local_variable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module TerraformConfig
class LocalVariable < Base
attr_reader :variables

def initialize(**variables)
super()

@variables = variables
end

def to_tf
block :locals do
variables.each do |var, value|
argument var, value
end
end
end
end
end
4 changes: 0 additions & 4 deletions lib/core/terraform_config/required_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ def initialize(name:, org:, **options)

def to_tf
block :terraform do
block :cloud do
argument :organization, org
end

block :required_providers do
argument name, options
end
Expand Down
Loading

0 comments on commit 11ed4ff

Please sign in to comment.