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 config from volumeset templates #237

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
8 changes: 3 additions & 5 deletions lib/command/terraform/generate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def generate_common_configs

def generate_app_configs
Array(config.app || config.apps.keys).each do |app|
config.instance_variable_set(:@app, app)
config.instance_variable_set(:@app, app.to_s)
generate_app_config
end
end
Expand All @@ -44,11 +44,9 @@ def generate_app_config

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 secret policy].include?(template["kind"])

File.write(terraform_app_dir.join(generator.filename), generator.tf_config.to_tf, mode: "a+")
rescue TerraformConfig::Generator::InvalidTemplateError => e
Shell.warn(e.message)
end
end

Expand Down
117 changes: 64 additions & 53 deletions lib/core/terraform_config/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,85 @@

module TerraformConfig
class Generator
SUPPORTED_TEMPLATE_KINDS = %w[gvc secret identity policy volumeset].freeze

class InvalidTemplateError < ArgumentError; end

attr_reader :config, :template

def initialize(config:, template:)
@config = config
@template = template.deep_underscore_keys.deep_symbolize_keys
validate_template_kind!
end

def filename # rubocop:disable Metrics/MethodLength
case kind
when "gvc"
"gvc.tf"
when "secret"
"secrets.tf"
when "identity"
"identities.tf"
when "policy"
"policies.tf"
else
raise "Unsupported template kind - #{kind}"
end
def filename
return "gvc.tf" if kind == "gvc"

"#{kind.pluralize}.tf"
end

def tf_config
method_name = :"#{kind}_config"
raise "Unsupported template kind - #{kind}" unless self.class.private_method_defined?(method_name)

send(method_name)
config_class.new(**config_params)
end

private

def kind
@kind ||= template[:kind]
def validate_template_kind!
return if SUPPORTED_TEMPLATE_KINDS.include?(kind)

raise InvalidTemplateError, "Unsupported template kind: #{kind}"
end

def config_class
if kind == "volumeset"
TerraformConfig::VolumeSet
else
TerraformConfig.const_get(kind.capitalize)
end
end

def config_params
send("#{kind}_config_params")
end

def gvc_config # rubocop:disable Metrics/MethodLength
TerraformConfig::Gvc.new(
**template
.slice(:name, :description, :tags)
.merge(
env: gvc_env,
pull_secrets: gvc_pull_secrets,
locations: gvc_locations,
domain: template.dig(:spec, :domain),
load_balancer: template.dig(:spec, :load_balancer)
)
)
def gvc_config_params
template
.slice(:name, :description, :tags)
.merge(
env: gvc_env,
pull_secrets: gvc_pull_secrets,
locations: gvc_locations,
domain: template.dig(:spec, :domain),
load_balancer: template.dig(:spec, :load_balancer)
)
end

def identity_config
TerraformConfig::Identity.new(**template.slice(:name, :description, :tags).merge(gvc: gvc))
def identity_config_params
template.slice(:name, :description, :tags).merge(gvc: gvc)
end

def secret_config
TerraformConfig::Secret.new(**template.slice(:name, :description, :type, :data, :tags))
def secret_config_params
template.slice(:name, :description, :type, :data, :tags)
end

def policy_config
TerraformConfig::Policy.new(
**template
.slice(:name, :description, :tags, :target, :target_kind, :target_query)
.merge(gvc: gvc, target_links: policy_target_links, bindings: policy_bindings)
)
def policy_config_params
template
.slice(:name, :description, :tags, :target, :target_kind, :target_query)
.merge(gvc: gvc, target_links: policy_target_links, bindings: policy_bindings)
end

def volumeset_config_params
specs = %i[
initial_capacity
performance_class
file_system_type
storage_class_suffix
snapshots
autoscaling
].to_h { |key| [key, template.dig(:spec, key)] }

template.slice(:name, :description, :tags).merge(gvc: gvc).merge(specs)
end

# GVC name matches application name
Expand All @@ -73,35 +89,30 @@ def gvc
end

def gvc_pull_secrets
template.dig(:spec, :pull_secret_links)&.map do |secret_link|
secret_name = secret_link.split("/").last
"cpln_secret.#{secret_name}.name"
end
template.dig(:spec, :pull_secret_links)&.map { |secret_link| "cpln_secret.#{secret_link.split('/').last}.name" }
end

def gvc_env
template.dig(:spec, :env).to_h { |env_var| [env_var[:name], env_var[:value]] }
end

def gvc_locations
template.dig(:spec, :static_placement, :location_links)&.map do |location_link|
location_link.split("/").last
end
template.dig(:spec, :static_placement, :location_links)&.map { |location_link| location_link.split("/").last }
end

# //secret/secret-name -> secret-name
def policy_target_links
template[:target_links]&.map do |target_link|
target_link.split("/").last
end
template[:target_links]&.map { |target_link| target_link.split("/").last }
end

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

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

module TerraformConfig
class VolumeSet < Base # rubocop:disable Metrics/ClassLength
PERFORMANCE_CLASSES = %w[general-purpose-ssd high-throughput-ssd].freeze
FILE_SYSTEM_TYPES = %w[xfs ext4].freeze
MIN_CAPACITY = 10
MIN_SCALING_FACTOR = 1.1

attr_reader :gvc, :name, :initial_capacity, :performance_class, :file_system_type,
:storage_class_suffix, :description, :tags, :snapshots, :autoscaling

def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
gvc:,
name:,
initial_capacity:,
performance_class:,
file_system_type:,
storage_class_suffix: nil,
description: nil,
tags: nil,
snapshots: nil,
autoscaling: nil
)
zzaakiirr marked this conversation as resolved.
Show resolved Hide resolved
super()

@gvc = gvc
@name = name
@initial_capacity = initial_capacity
@performance_class = performance_class
@file_system_type = file_system_type
@storage_class_suffix = storage_class_suffix
@description = description
@tags = tags
@snapshots = snapshots
@autoscaling = autoscaling

validate_attributes!
end

def to_tf
block :resource, :cpln_volume_set, name do
base_arguments_tf
snapshots_tf
autoscaling_tf
end
end

private

def validate_attributes!
validate_initial_capacity!
validate_performance_class!
validate_file_system_type!
validate_autoscaling! if autoscaling
end

def validate_initial_capacity!
raise ArgumentError, "Initial capacity must be numeric" unless initial_capacity.is_a?(Numeric)
return if initial_capacity >= MIN_CAPACITY

raise ArgumentError, "Initial capacity should be >= #{MIN_CAPACITY}"
end

def validate_performance_class!
return if PERFORMANCE_CLASSES.include?(performance_class.to_s)

raise ArgumentError,
"Invalid performance class: #{performance_class}. Choose from #{PERFORMANCE_CLASSES.join(', ')}"
end

def validate_file_system_type!
return if FILE_SYSTEM_TYPES.include?(file_system_type.to_s)

raise ArgumentError, "Invalid file system type: #{file_system_type}. Choose from #{FILE_SYSTEM_TYPES.join(', ')}"
end

def validate_autoscaling!
validate_max_capacity!
validate_min_free_percentage!
validate_scaling_factor!
end

def validate_max_capacity!
max_capacity = autoscaling.fetch(:max_capacity, nil)
return if max_capacity.nil?

raise ArgumentError, "autoscaling.max_capacity must be numeric" unless max_capacity.is_a?(Numeric)
return if max_capacity >= MIN_CAPACITY

raise ArgumentError, "autoscaling.max_capacity should be >= #{MIN_CAPACITY}"
end

def validate_min_free_percentage!
min_free_percentage = autoscaling.fetch(:min_free_percentage, nil)
return if min_free_percentage.nil?

raise ArgumentError, "autoscaling.min_free_percentage must be numeric" unless min_free_percentage.is_a?(Numeric)
return if min_free_percentage.between?(1, 100)

raise ArgumentError, "autoscaling.min_free_percentage should be between 1 and 100"
end

def validate_scaling_factor!
scaling_factor = autoscaling.fetch(:scaling_factor, nil)
return if scaling_factor.nil?

raise ArgumentError, "autoscaling.scaling_factor must be numeric" unless scaling_factor.is_a?(Numeric)
return if scaling_factor >= MIN_SCALING_FACTOR

raise ArgumentError, "autoscaling.scaling_factor should be >= #{MIN_SCALING_FACTOR}"
end

def base_arguments_tf
argument :gvc, gvc

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

argument :initial_capacity, initial_capacity
argument :performance_class, performance_class
argument :storage_class_suffix, storage_class_suffix, optional: true
argument :file_system_type, file_system_type
end

def snapshots_tf
return if snapshots.nil?

block :snapshots do
%i[create_final_snapshot retention_duration schedule].each do |arg_name|
argument arg_name, snapshots.fetch(arg_name, nil), optional: true
end
end
end

def autoscaling_tf
return if autoscaling.nil?

block :autoscaling do
%i[max_capacity min_free_percentage scaling_factor].each do |arg_name|
argument arg_name, autoscaling.fetch(arg_name, nil), optional: true
end
end
end
end
end
8 changes: 8 additions & 0 deletions lib/patches/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,13 @@ def unindent
def underscore
gsub("::", "/").gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').tr("-", "_").downcase
end

# Covers only simple cases and used for pluralizing controlplane template kinds (`gvc`, `secret`, `policy`, etc.)
def pluralize
return self if empty?
return "#{self[...-1]}ies" if end_with?("y")

"#{self}s"
end
end
# rubocop:enable Style/OptionalBooleanParameter, Lint/UnderscorePrefixedVariableName
20 changes: 19 additions & 1 deletion spec/command/terraform/generate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,24 @@
end
end

context "when InvalidTemplateError is raised" do
before do
allow_any_instance_of(TerraformConfig::Generator).to receive(:tf_config).and_raise( # rubocop:disable RSpec/AnyInstance
TerraformConfig::Generator::InvalidTemplateError, "Invalid template: error message"
)
end

it "generates common config files and warns about invalid template" do
config_file_paths.each { |config_file_path| expect(config_file_path).not_to exist }

expect(result[:status]).to eq(0)
expect(result[:stderr]).to include("Invalid template: error message")

expect(common_config_files).to all(exist)
app_config_files.each { |config_file_path| expect(config_file_path).not_to exist }
end
end

def config_file_paths
common_config_files + app_config_files
end
Expand All @@ -95,7 +113,7 @@ def common_config_files
end

def app_config_files
%w[gvc.tf identities.tf secrets.tf policies.tf].map do |config_file_path|
%w[gvc.tf identities.tf secrets.tf policies.tf volumesets.tf].map do |config_file_path|
TERRAFORM_CONFIG_DIR_PATH.join(app, config_file_path)
end
end
Expand Down
Loading
Loading