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 configs from workload templates #240

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
rafaelgomesxyz marked this conversation as resolved.
Show resolved Hide resolved
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
zzaakiirr marked this conversation as resolved.
Show resolved Hide resolved
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)
rafaelgomesxyz marked this conversation as resolved.
Show resolved Hide resolved
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
zzaakiirr marked this conversation as resolved.
Show resolved Hide resolved

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
zzaakiirr marked this conversation as resolved.
Show resolved Hide resolved
end

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

def block_declaration(name, labels)
Expand Down
79 changes: 69 additions & 10 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
zzaakiirr marked this conversation as resolved.
Show resolved Hide resolved

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
rafaelgomesxyz marked this conversation as resolved.
Show resolved Hide resolved
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
@tf_config ||= config_class.new(**config_params)
end

def config_class
if kind == "volumeset"
TerraformConfig::VolumeSet
Expand Down Expand Up @@ -83,6 +104,37 @@ 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)

if value
case key
when :local_options
value[:location] = value.delete(:location).split("/").last
when :security_options
value[:file_system_group_id] = value.delete(:filesystem_group_id)
end
end

[arg_name, value]
end
end

# GVC name matches application name
def gvc
"cpln_gvc.#{config.app}.name"
Expand Down Expand Up @@ -111,6 +163,13 @@ def policy_bindings
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
30 changes: 30 additions & 0 deletions lib/core/terraform_config/local_variable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module TerraformConfig
class LocalVariable < Base
VARIABLE_NAME_REGEX = /\A[a-zA-Z][a-zA-Z0-9_]*\z/.freeze

attr_reader :variables

def initialize(**variables)
super()

@variables = variables
validate_variables!
end

def to_tf
block :locals do
variables.each do |var, value|
argument var, value
end
end
end

private

def validate_variables!
raise ArgumentError, "Variables cannot be empty" if variables.empty?
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
Loading