Skip to content

Commit

Permalink
Merge pull request #89 from putmanoj/add-dialog-fields-based-on-terra…
Browse files Browse the repository at this point in the history
…form-type-constraints

Add dialog fields based on terraform type constraints
  • Loading branch information
agrare authored Jan 13, 2025
2 parents 07ce648 + 5fbef6b commit dd00688
Show file tree
Hide file tree
Showing 7 changed files with 604 additions and 34 deletions.
105 changes: 98 additions & 7 deletions app/models/dialog/terraform_template_service_dialog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ def create_dialog(label, terraform_template, extra_vars)
end
end

JSONSTR_LIST_REGEX = '^\[[\W\w]*\]$'.freeze # list of strings or objects
JSONSTR_OBJECT_REGEX = '^\{[\W\w]*\}$'.freeze # map or object

NUMBER_REGEX = '^[0-9]+$|^[0-9]+[\.]{1}[0-9]+$'.freeze # integer or decimal point number

private

def add_template_variables_group(tab, position, terraform_template)
Expand All @@ -42,17 +47,35 @@ def add_template_variables_group(tab, position, terraform_template)
:position => position
).tap do |dialog_group|
input_vars.each_with_index do |(var_info), index|
key, value, required, readonly, hidden, label, description = var_info.values_at(
"name", "default", "required", "immutable", "hidden", "label", "description"
key, value, required, readonly, hidden, label, description, type = var_info.values_at(
"name", "default", "required", "immutable", "hidden", "label", "description", "type"
)
# TODO: use these when adding variable field
# type, secured = var_info.values_at("type", "secured")
# TODO: use 'hidden' & 'secured' attributes, when adding variable field

next if hidden

add_variable_field(
key, value, dialog_group, index, label, description, required, readonly
)
case type
when "boolean"
add_check_box_field(
key, value, dialog_group, index, label, description, readonly
)
when "number"
add_number_variable_field(
key, value, dialog_group, index, label, description, required, readonly
)
when "map"
add_json_variable_field(
key, value, dialog_group, index, label, description, required, readonly, :is_list => false
)
when "list"
add_json_variable_field(
key, value, dialog_group, index, label, description, required, readonly, :is_list => true
)
else
add_variable_field(
key, value, dialog_group, index, label, description, required, readonly
)
end
end
end
end
Expand Down Expand Up @@ -88,5 +111,73 @@ def add_variable_field(key, value, group, position, label, description, required
:read_only => read_only
)
end

def add_json_variable_field(key, value, group, position, label, description, required, read_only, is_list: false)
value = JSON.pretty_generate(value) if [Hash, Array].include?(value.class)
description = key if description.blank?

group.dialog_fields.build(
:type => 'DialogFieldTextAreaBox',
:name => key.to_s,
:data_type => 'string',
:display => 'edit',
:required => required,
:default_value => value,
:label => label,
:description => description,
:reconfigurable => true,
:position => position,
:dialog_group => group,
:read_only => read_only,
:validator_type => 'regex',
:validator_rule => is_list ? JSONSTR_LIST_REGEX : JSONSTR_OBJECT_REGEX,
:validator_message => "This field value must be a JSON #{is_list ? 'List' : 'Object or Map'}"
)
end

def add_number_variable_field(key, value, group, position, label, description, required, read_only)
description = key if description.blank?

group.dialog_fields.build(
:type => 'DialogFieldTextBox',
:name => key.to_s,
:data_type => 'string',
:display => 'edit',
:required => required,
:default_value => value,
:label => label,
:description => description,
:reconfigurable => true,
:position => position,
:dialog_group => group,
:read_only => read_only,
:validator_type => 'regex',
:validator_rule => NUMBER_REGEX,
:validator_message => "This field value must be a number"
)
end

def add_check_box_field(key, value, group, position, label, description, read_only)
value = to_boolean(value)
description = key if description.blank?

group.dialog_fields.build(
:type => "DialogFieldCheckBox",
:name => key.to_s,
:data_type => "boolean",
:default_value => value,
:label => label,
:description => description,
:reconfigurable => true,
:position => position,
:dialog_group => group,
:read_only => read_only
)
end

def to_boolean(value)
require 'active_model/type'
ActiveModel::Type::Boolean.new.cast(value)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,18 @@ def execute
Terraform::Runner.delete_stack(
options[:terraform_stack_id],
template_path,
:input_vars => decrypt_vars(input_vars),
:credentials => credentials,
:env_vars => options[:env_vars]
:input_vars => decrypt_vars(input_vars),
:input_vars_type_constraints => input_vars_type_constraints,
:credentials => credentials,
:env_vars => options[:env_vars]
)
else
response = Terraform::Runner.create_stack(
template_path,
:input_vars => decrypt_vars(input_vars),
:credentials => credentials,
:env_vars => options[:env_vars]
:input_vars => decrypt_vars(input_vars),
:input_vars_type_constraints => input_vars_type_constraints,
:credentials => credentials,
:env_vars => options[:env_vars]
)
options[:terraform_stack_id] = response.stack_id
save!
Expand Down Expand Up @@ -155,4 +157,15 @@ def cleanup_git_repository
rescue Errno::ENOENT
nil
end

# Returns key/value(type constraints object, from Terraform Runner) pair.
# @return [Hash]
def input_vars_type_constraints
require 'json'
payload = JSON.parse(template.payload)
(payload['input_vars'] || []).index_by { |v| v['name'] }
rescue => error
_log.error("Failure in parsing payload for template/#{template.id}, caused by #{error.message}")
{}
end
end
33 changes: 20 additions & 13 deletions lib/terraform/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,24 @@ def available?
#
# @param template_path [String] (required) path to the terraform template directory.
# @param input_vars [Hash] (optional) key/value pairs as input variables for the terraform-runner run job.
# @param input_vars_type_constraints
# [Hash] (optional) key/value(type constraints object, from Terraform Runner) pair.
# @param tags [Hash] (optional) key/value pairs tags for terraform-runner Provisioned resources.
# @param credentials [Array] (optional) List of Authentication objects for the terraform run job.
# @param env_vars [Hash] (optional) key/value pairs used as environment variables, for terraform-runner run job.
#
# @return [Terraform::Runner::ResponseAsync] Response object of terraform-runner api call
def create_stack(template_path, input_vars: {}, tags: nil, credentials: [], env_vars: {})
def create_stack(template_path, input_vars: {}, input_vars_type_constraints: [], tags: nil, credentials: [], env_vars: {})
_log.debug("Run_aysnc/create_stack for template: #{template_path}")
if template_path.present?
input_params = ApiParams.to_normalized_cam_parameters(input_vars, input_vars_type_constraints)

response = create_stack_job(
template_path,
:input_vars => input_vars,
:tags => tags,
:credentials => credentials,
:env_vars => env_vars
:input_params => input_params,
:tags => tags,
:credentials => credentials,
:env_vars => env_vars
)
Terraform::Runner::ResponseAsync.new(response.stack_id)
else
Expand All @@ -45,19 +49,22 @@ def create_stack(template_path, input_vars: {}, tags: nil, credentials: [], env_
# @param stack_id [String] (optional) required, if running ResourceAction::RETIREMENT action, used by Terraform-Runner stack_delete job.
# @param template_path [String] (required) path to the terraform template directory.
# @param input_vars [Hash] (optional) key/value pairs as input variables for the terraform-runner run job.
# @param input_vars_type_constraints
# [Hash] (optional) key/value(type constraints object, from Terraform Runner) pair.
# @param credentials [Array] (optional) List of Authentication objects for the terraform run job.
# @param env_vars [Hash] (optional) key/value pairs used as environment variables, for terraform-runner run job.
#
# @return [Terraform::Runner::ResponseAsync] Response object of terraform-runner api call
def delete_stack(stack_id, template_path, input_vars: {}, credentials: [], env_vars: {})
def delete_stack(stack_id, template_path, input_vars: {}, input_vars_type_constraints: [], credentials: [], env_vars: {})
if stack_id.present? && template_path.present?
_log.debug("Run_aysnc/delete_stack('#{stack_id}') for template: #{template_path}")
input_params = ApiParams.to_normalized_cam_parameters(input_vars, input_vars_type_constraints)
response = delete_stack_job(
stack_id,
template_path,
:input_vars => input_vars,
:credentials => credentials,
:env_vars => env_vars
:input_params => input_params,
:credentials => credentials,
:env_vars => env_vars
)
Terraform::Runner::ResponseAsync.new(response.stack_id)
else
Expand Down Expand Up @@ -153,7 +160,7 @@ def provider_connection_parameters(credentials)
# Create TerraformRunner Stack Job
def create_stack_job(
template_path,
input_vars: {},
input_params: [],
tags: nil,
credentials: [],
env_vars: {},
Expand All @@ -169,7 +176,7 @@ def create_stack_job(
:name => name,
:tenantId => tenant_id,
:templateZipFile => encoded_zip_file,
:parameters => ApiParams.to_cam_parameters(input_vars)
:parameters => input_params,
}

http_response = terraform_runner_client.post(
Expand All @@ -185,7 +192,7 @@ def create_stack_job(
def delete_stack_job(
stack_id,
template_path,
input_vars: {},
input_params: [],
credentials: [],
env_vars: {}
)
Expand All @@ -200,7 +207,7 @@ def delete_stack_job(
:name => name,
:tenantId => tenant_id,
:templateZipFile => encoded_zip_file,
:parameters => ApiParams.to_cam_parameters(input_vars)
:parameters => input_params,
}

http_response = terraform_runner_client.post(
Expand Down
74 changes: 73 additions & 1 deletion lib/terraform/runner/api_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def self.add_param_if_present(param_list, param_value, param_name, is_secured: f
param_list
end

# convert to format required by terraform-runner api
# Convert to format required by terraform-runner api
def self.to_cam_param(param_name, param_value, is_secured: false)
{
'name' => param_name,
Expand All @@ -34,6 +34,7 @@ def self.to_cam_param(param_name, param_value, is_secured: false)
#
# @param vars [Hash] Hash with key/value pairs that will be passed as input variables to the
# terraform-runner run
#
# @return [Array] Array of {:name,:value}
def self.to_cam_parameters(vars)
return [] if vars.nil?
Expand All @@ -42,6 +43,77 @@ def self.to_cam_parameters(vars)
to_cam_param(key, value)
end
end

require 'set'
TRUE_VALUES = ['T', 't', true, 'true', 'True', 'TRUE'].to_set

# Normalize variables values, from ManageIQ values to Terraform Runner supported values
# @param input_vars [Hash] key/value pairs as input variables for the terraform-runner run job.
# @param type_constraints [Hash] key/value(type constraints object, from Terraform Runner) pairs.
#
# @return [Array] Array of param objects [{name,value,secured}]
def self.to_normalized_cam_parameters(input_vars, type_constraints)
input_vars.map do |k, v|
v, is_secured = normalized_param_value(k, v, type_constraints[k])

to_cam_param(k, v, :is_secured => is_secured)
end
end

def self.normalized_param_value(key, value, param_type_constraint)
is_secured = false
if param_type_constraint.present?
param_secured, param_type, param_required = param_type_constraint.values_at('secured', 'type', 'required')

is_secured = TRUE_VALUES.include?(param_secured)
is_required = TRUE_VALUES.include?(param_required)

case param_type
when "boolean"
value = TRUE_VALUES.include?(value)

when "map"
value = parse_json_value(key, value, :expected_type => Hash, :is_required => is_required)

when "list"
value = parse_json_value(key, value, :expected_type => Array, :is_required => is_required)

else
# string or number(string)
# (number as string, is implicitly converted by terraform, so no conversion is requried here)
if value.blank? && is_required
raise "The variable '#{key}', cannot be empty"
end
end
end

[value, is_secured, param_type, is_required]
end

def self.parse_json_value(key, value, expected_type: Array, is_required: false)
if value.kind_of?(String)
if value.empty?
value = nil
else
require 'json'
begin
value = JSON.parse(value)
rescue JSON::ParserError
raise "The variable '#{key}' does not have valid #{expected_type.name} value"
end
end
end

if value.nil?
if is_required
raise "The variable '#{key}' does not have valid #{expected_type.name} value"
end
elsif !value.kind_of?(expected_type)
raise "The variable '#{key}' does not have valid #{expected_type.name} value"
end

value
end
end
end
end
Loading

0 comments on commit dd00688

Please sign in to comment.