Skip to content

Commit

Permalink
Generate Terraform config from gvc & identity templates
Browse files Browse the repository at this point in the history
  • Loading branch information
zzaakiirr committed Aug 30, 2024
1 parent 3382a79 commit 66e4177
Show file tree
Hide file tree
Showing 12 changed files with 379 additions and 10 deletions.
2 changes: 1 addition & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ cpflow setup-app -a $APP_NAME
- Generates terraform configuration files based on `controlplane.yml` and `templates/` config
```sh
cpflow terraform generate
cpflow terraform generate -a $APP_NAME
```
### `version`
Expand Down
19 changes: 18 additions & 1 deletion lib/command/terraform/generate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,26 @@ module Terraform
class Generate < Base
SUBCOMMAND_NAME = "terraform"
NAME = "generate"
OPTIONS = [
app_option(required: true)
].freeze
DESCRIPTION = "Generates terraform configuration files"
LONG_DESCRIPTION = <<~DESC
- Generates terraform configuration files based on `controlplane.yml` and `templates/` config
DESC
WITH_INFO_HEADER = false
VALIDATIONS = [].freeze

def call
File.write(terraform_dir.join("providers.tf"), cpln_provider.to_tf)

templates.each do |template|
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"])

File.write(terraform_dir.join(generator.filename), generator.tf_config.to_tf, mode: "a+")
end
end

private
Expand All @@ -22,8 +33,14 @@ def cpln_provider
TerraformConfig::RequiredProvider.new("cpln", source: "controlplane-com/cpln", version: "~> 1.0")
end

def templates
parser = TemplateParser.new(self)
parser.parse(Dir["#{parser.template_dir}/*.yml"])
end

def terraform_dir
@terraform_dir ||= Cpflow.root_path.join("terraform").tap do |path|
FileUtils.rm_rf(path)
FileUtils.mkdir_p(path)
end
end
Expand Down
6 changes: 4 additions & 2 deletions lib/core/terraform_config/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module TerraformConfig
module Dsl
extend Forwardable

REFERENCE_PATTERN = /^(vars|locals|cpln_\w+)\./.freeze

def_delegators :current_context, :put, :output

def block(name, *labels)
Expand Down Expand Up @@ -44,7 +46,7 @@ def tf_value(value)
end

def expression?(value)
value.start_with?("var.") || value.start_with?("locals.")
value.match?(REFERENCE_PATTERN)
end

def block_declaration(name, labels)
Expand All @@ -62,7 +64,7 @@ def initialize
end

def put(content, indent: 0)
@output += content.indent(indent)
@output += content.to_s.indent(indent)
end
end

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

module TerraformConfig
class Generator
attr_reader :config, :template

def initialize(config:, template:)
@config = config
@template = template
end

def filename
case template["kind"]
when "gvc"
"gvc.tf"
when "identity"
"identities.tf"
else
raise "Unsupported template kind - #{template['kind']}"
end
end

def tf_config
case template["kind"]
when "gvc"
gvc_config
when "identity"
identity_config
else
raise "Unsupported template kind - #{template['kind']}"
end
end

private

# rubocop:disable Metrics/MethodLength
def gvc_config
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")

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
)
end
# rubocop:enable Metrics/MethodLength

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"]
)
end

def 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|
location_link.split("/").last
end
end
end
end
57 changes: 57 additions & 0 deletions lib/core/terraform_config/gvc.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

module TerraformConfig
class Gvc < Base
attr_reader :name, :description, :tags, :domain, :locations, :pull_secrets, :env, :load_balancer

# rubocop:disable Metrics/ParameterLists
def initialize(
name:,
description: nil,
tags: nil,
domain: nil,
locations: nil,
pull_secrets: nil,
env: nil,
load_balancer: nil
)
super()

@name = name
@description = description
@tags = tags
@domain = domain
@locations = locations
@pull_secrets = pull_secrets
@env = env
@load_balancer = load_balancer&.transform_keys { |k| k.to_s.underscore.to_sym }
end
# rubocop:enable Metrics/ParameterLists

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

argument :domain, domain, optional: true
argument :locations, locations, optional: true
argument :pull_secrets, pull_secrets, optional: true
argument :env, env, optional: true

load_balancer_tf
end
end

private

def load_balancer_tf
return if load_balancer.nil?

block :load_balancer do
argument :dedicated, load_balancer.fetch(:dedicated)
argument :trusted_proxies, load_balancer[:trusted_proxies], optional: true
end
end
end
end
27 changes: 27 additions & 0 deletions lib/core/terraform_config/identity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module TerraformConfig
class Identity < Base
attr_reader :gvc, :name, :description, :tags

def initialize(gvc:, name:, description: nil, tags: nil)
super()

@gvc = gvc
@name = name
@description = description
@tags = tags
end

def to_tf
block :resource, :cpln_identity, name do
argument :gvc, gvc

argument :name, name
argument :description, description, optional: true

argument :tags, tags, optional: true
end
end
end
end
2 changes: 1 addition & 1 deletion lib/cpflow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def self.klass_for(subcommand_name)
long_desc(long_description)

command_options.each do |option|
params = process_option_params(option[:params])
params = Cpflow::Cli.process_option_params(option[:params])
method_option(option[:name], **params)
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/patches/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@ def indent!(amount, indent_string = nil, indent_empty_lines = false)
def unindent
gsub(/^#{scan(/^[ \t]+(?=\S)/).min}/, "")
end

def underscore
gsub(/(.)([A-Z])/, '\1_\2').downcase
end
end
# rubocop:enable Style/OptionalBooleanParameter, Lint/UnderscorePrefixedVariableName
15 changes: 10 additions & 5 deletions spec/command/terraform/generate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
TERRAFORM_CONFIG_DIR_PATH = GENERATOR_PLAYGROUND_PATH.join("terraform")

describe Command::Terraform::Generate do
let!(:app) { dummy_test_app }

before do
FileUtils.rm_r(GENERATOR_PLAYGROUND_PATH) if Dir.exist?(GENERATOR_PLAYGROUND_PATH)
FileUtils.mkdir_p GENERATOR_PLAYGROUND_PATH
Expand All @@ -20,11 +22,14 @@
FileUtils.rm_r GENERATOR_PLAYGROUND_PATH
end

it "generates terraform config files" do
providers_config_file_path = TERRAFORM_CONFIG_DIR_PATH.join("providers.tf")
it "generates terraform config files", :aggregate_failures do
config_file_paths = %w[providers.tf gvc.tf identities.tf].map do |config_file_path|
TERRAFORM_CONFIG_DIR_PATH.join(config_file_path)
end

config_file_paths.each { |config_file_path| expect(config_file_path).not_to exist }

expect(providers_config_file_path).not_to exist
run_cpflow_command(described_class::SUBCOMMAND_NAME, described_class::NAME)
expect(providers_config_file_path).to exist
run_cpflow_command(described_class::SUBCOMMAND_NAME, described_class::NAME, "-a", app)
expect(config_file_paths).to all(exist)
end
end
92 changes: 92 additions & 0 deletions spec/core/terraform_config/generator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# frozen_string_literal: true

require "spec_helper"

describe TerraformConfig::Generator do
let(:generator) { described_class.new(config: config, template: template) }

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

context "when template's kind is gvc" do
let(:template) do
{
"kind" => "gvc",
"name" => config.app,
"description" => "description",
"tags" => { "tag1" => "tag1_value", "tag2" => "tag2_value" },
"spec" => {
"domain" => "app.example.com",
"env" => [
{
"name" => "DATABASE_URL",
"value" => "postgres://the_user:the_password@postgres.#{config.app}.cpln.local:5432/#{config.app}"
},
{
"name" => "RAILS_ENV",
"value" => "production"
},
{
"name" => "RAILS_SERVE_STATIC_FILES",
"value" => "true"
}
],
"staticPlacement" => {
"locationLinks" => ["/org/#{config.org}/location/aws-us-east-2"]
},
"pullSecretLinks" => ["/org/#{config.org}/secret/some-secret"],
"loadBalancer" => {
"dedicated" => true,
"trustedProxies" => 1
}
}
}
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::Gvc)

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

expect(tf_config.domain).to eq("app.example.com")
expect(tf_config.locations).to eq(["aws-us-east-2"])
expect(tf_config.pull_secrets).to eq(["cpln_secret.some-secret.name"])
expect(tf_config.env).to eq(
{
"DATABASE_URL" => "postgres://the_user:the_password@postgres.#{config.app}.cpln.local:5432/#{config.app}",
"RAILS_ENV" => "production",
"RAILS_SERVE_STATIC_FILES" => "true"
}
)
expect(tf_config.load_balancer).to eq({ "dedicated" => true, "trusted_proxies" => 1 })

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

context "when template's kind is identity" do
let(:template) do
{
"kind" => "identity",
"name" => "identity-name",
"description" => "description",
"tags" => { "tag1" => "tag1_value", "tag2" => "tag2_value" }
}
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::Identity)

expect(tf_config.name).to eq("identity-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("identities.tf")
end
end
end
Loading

0 comments on commit 66e4177

Please sign in to comment.