diff --git a/app/models/dialog/terraform_template_service_dialog.rb b/app/models/dialog/terraform_template_service_dialog.rb new file mode 100644 index 00000000..e5daf79e --- /dev/null +++ b/app/models/dialog/terraform_template_service_dialog.rb @@ -0,0 +1,85 @@ +class Dialog + class TerraformTemplateServiceDialog + def self.create_dialog(label, terraform_template, extra_vars) + new.create_dialog(label, terraform_template, extra_vars) + end + + # This dialog is to be used by a terraform template service item + def create_dialog(label, terraform_template, extra_vars) + Dialog.new(:label => label, :buttons => "submit,cancel").tap do |dialog| + tab = dialog.dialog_tabs.build(:display => "edit", :label => "Basic Information", :position => 0) + position = 0 + if terraform_template.present? + add_template_variables_group(tab, position, terraform_template) + position += 1 + end + if extra_vars.present? + add_variables_group(tab, position, extra_vars) + end + dialog.save! + end + end + + private + + def add_template_variables_group(tab, position, terraform_template) + require "json" + template_info = JSON.parse(terraform_template.payload) + input_vars = template_info["input_vars"] + + return if input_vars.nil? + + tab.dialog_groups.build( + :display => "edit", + :label => "Terraform Template Variables", + :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" + ) + # TODO: use these when adding variable field + # type, secured = var_info.values_at("type", "secured") + + next if hidden + + add_variable_field( + key, value, dialog_group, index, label, description, required, readonly + ) + end + end + end + + def add_variables_group(tab, position, extra_vars) + tab.dialog_groups.build( + :display => "edit", + :label => "Extra Variables", + :position => position + ).tap do |dialog_group| + extra_vars.transform_values { |val| val[:default] }.each_with_index do |(key, value), index| + add_variable_field(key, value, dialog_group, index, key, key, false, false) + end + end + end + + def add_variable_field(key, value, group, position, label, description, required, read_only) + value = value.to_json if [Hash, Array].include?(value.class) + 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 + ) + end + end +end diff --git a/app/models/manageiq/providers/embedded_terraform/automation_manager/configuration_script_source.rb b/app/models/manageiq/providers/embedded_terraform/automation_manager/configuration_script_source.rb index 3eba437a..77f452ca 100644 --- a/app/models/manageiq/providers/embedded_terraform/automation_manager/configuration_script_source.rb +++ b/app/models/manageiq/providers/embedded_terraform/automation_manager/configuration_script_source.rb @@ -37,7 +37,7 @@ def sync update!(:status => "successful", :last_updated_on => Time.zone.now, :last_update_error => nil) rescue => error update!(:status => "error", :last_updated_on => Time.zone.now, :last_update_error => error) - raise error + raise end # Return Template name, using relative_path's basename prefix, @@ -75,8 +75,10 @@ def self.template_name_from_git_repo_url(git_repo_url, relative_path) def find_templates_in_git_repo template_dirs = {} + # checkout repo, for sending files to terraform-runner to parse for input/ouput vars. + git_checkout_tempdir = checkout_git_repo + # traverse through files in git-worktree - git_repository.update_repo git_repository.with_worktree do |worktree| worktree.ref = scm_branch @@ -85,22 +87,53 @@ def find_templates_in_git_repo .group_by { |file| File.dirname(file) } .select { |_dir, files| files.any? { |f| f.end_with?(".tf", ".tf.json") } } .transform_values! { |files| files.map { |f| File.basename(f) } } - .each do |parent_dir, files| - name = self.class.template_name_from_git_repo_url(git_repository.url, parent_dir) + .each do |relative_path, files| + name = self.class.template_name_from_git_repo_url(git_repository.url, relative_path) + + template_full_path = File.join(git_checkout_tempdir, relative_path) - # TODO: add parsing for input/output vars - input_vars = nil - output_vars = nil + input_vars, output_vars, terraform_version = parse_vars_in_template(template_full_path) template_dirs[name] = { - :relative_path => parent_dir, - :files => files, - :input_vars => input_vars, - :output_vars => output_vars + :relative_path => relative_path, + :files => files, + :input_vars => input_vars, + :output_vars => output_vars, + :terraform_version => terraform_version, } end end template_dirs + rescue => error + _log.error("Failing scaning for terraform templates in the git repo: #{error}") + raise + ensure + cleanup_git_repo(git_checkout_tempdir) + end + + # Parse template and return input-vars, output-vars & terraform-version + def parse_vars_in_template(template_path) + response = Terraform::Runner.parse_template_variables(template_path) + return response['template_input_params'], response['template_output_params'], response['terraform_version'] + end + + # checkout git repo to temp dir + def checkout_git_repo + git_checkout_tempdir = Dir.mktmpdir("embedded-terraform-runner-git") + + _log.debug("Checking out git repository to #{git_checkout_tempdir}...") + checkout_git_repository(git_checkout_tempdir) + git_checkout_tempdir + end + + # clean temp dir + def cleanup_git_repo(git_checkout_tempdir) + return if git_checkout_tempdir.nil? + + _log.debug("Cleaning up git repository checked out at #{git_checkout_tempdir}...") + FileUtils.rm_rf(git_checkout_tempdir) + rescue Errno::ENOENT + nil end end diff --git a/app/models/manageiq/providers/embedded_terraform/automation_manager/template.rb b/app/models/manageiq/providers/embedded_terraform/automation_manager/template.rb index 51ebaeac..b7df59f0 100644 --- a/app/models/manageiq/providers/embedded_terraform/automation_manager/template.rb +++ b/app/models/manageiq/providers/embedded_terraform/automation_manager/template.rb @@ -2,7 +2,7 @@ class ManageIQ::Providers::EmbeddedTerraform::AutomationManager::Template < Mana has_many :stacks, :class_name => "ManageIQ::Providers::EmbeddedTerraform::AutomationManager::Stack", :foreign_key => :configuration_script_base_id, :inverse_of => :configuration_script_payload, :dependent => :nullify def run(vars = {}, _userid = nil) - env_vars = vars.delete(:env) || {} + env_vars = vars.delete(:env) || {} credentials = vars.delete(:credentials) self.class.module_parent::Job.create_job(self, env_vars, vars, credentials).tap(&:signal_start) diff --git a/app/models/service_template_terraform_template.rb b/app/models/service_template_terraform_template.rb index a23e4569..8bd25e44 100644 --- a/app/models/service_template_terraform_template.rb +++ b/app/models/service_template_terraform_template.rb @@ -17,11 +17,28 @@ def self.create_catalog_item(options, _auth_user) transaction do create_from_options(options).tap do |service_template| + dialog_ids = service_template.send(:create_dialogs, config_info) + config_info.deep_merge!(dialog_ids) + service_template.options[:config_info] = config_info service_template.create_resource_actions(config_info) end end end + def update_catalog_item(options, auth_user = nil) + config_info = validate_update_config_info(options) + unless config_info + update!(options) + return reload + end + + config_info.deep_merge!(create_dialogs(config_info)) + + options[:config_info] = config_info + + super + end + def self.validate_config_info(info) info[:provision][:fqname] ||= default_provisioning_entry_point(SERVICE_TYPE_ATOMIC) if info.key?(:provision) info[:reconfigure][:fqname] ||= default_reconfiguration_entry_point if info.key?(:reconfigure) @@ -41,4 +58,39 @@ def terraform_template(action) ManageIQ::Providers::EmbeddedTerraform::AutomationManager::Template.find(template_id) end + + def create_dialogs(config_info) + dialog_hash = {} + + info = config_info[:provision] + if info + # create new dialog, if required for :provision action + if info.key?(:new_dialog_name) && !info.key?(:dialog_id) + provision_dialog_id = create_new_dialog(info[:new_dialog_name], terraform_template(:provision), info[:extra_vars]).id + dialog_hash[:provision] = {:dialog_id => provision_dialog_id} + else + provision_dialog_id = info[:dialog_id] + end + + # For :retirement & :reconfigure, we use the same dialog as in :provision action + dialog_hash = [:retirement, :reconfigure].each_with_object(dialog_hash) do |action, hash| + hash[action] = {:dialog_id => provision_dialog_id} + end + end + + dialog_hash + end + + private + + def create_new_dialog(dialog_name, terraform_template, extra_vars) + Dialog::TerraformTemplateServiceDialog.create_dialog(dialog_name, terraform_template, extra_vars) + end + + def validate_update_config_info(options) + opts = super + return unless options.key?(:config_info) + + self.class.send(:validate_config_info, opts) + end end diff --git a/lib/terraform/runner.rb b/lib/terraform/runner.rb index fe9f7165..dd0d76a8 100644 --- a/lib/terraform/runner.rb +++ b/lib/terraform/runner.rb @@ -59,6 +59,14 @@ def fetch_result_by_stack_id(stack_id) retrieve_stack_job(stack_id) end + # Parse Terraform Template input/output variables + # @param template_path [String] Path to the template we will want to parse for input/output variables + # @return Response(body) object of terraform-runner api/template/variables, + # - the response object had template_input_params, template_output_params and terraform_version + def parse_template_variables(template_path) + template_variables(template_path) + end + # ================================================= # TerraformRunner Stack-API interaction methods # ================================================= @@ -179,6 +187,26 @@ def encoded_zip_from_directory(template_path) end end + # Parse Variables in Terraform Template + def template_variables( + template_path + ) + _log.debug("prase template: #{template_path}") + encoded_zip_file = encoded_zip_from_directory(template_path) + + payload = { + :templateZipFile => encoded_zip_file, + } + + http_response = terraform_runner_client.post( + "api/template/variables", + *json_post_arguments(payload) + ) + + _log.debug("==== http_response.body: \n #{http_response.body}") + JSON.parse(http_response.body) + end + def jwt_token require "jwt" diff --git a/spec/lib/terraform/runner/data/responses/hello-world-variables-success.json b/spec/lib/terraform/runner/data/responses/hello-world-variables-success.json new file mode 100644 index 00000000..3ea73cd3 --- /dev/null +++ b/spec/lib/terraform/runner/data/responses/hello-world-variables-success.json @@ -0,0 +1,25 @@ +{ + "template_input_params": [ + { + "name": "name", + "label": "name", + "type": "string", + "description": "", + "required": true, + "secured": false, + "hidden": false, + "immutable": false, + "default": "World" + } + ], + "template_output_params": [ + { + "name": "greeting", + "label": "greeting", + "description": "", + "secured": false, + "hidden": false + } + ], + "terraform_version": ">= 1.1.0" + } \ No newline at end of file diff --git a/spec/lib/terraform/runner_spec.rb b/spec/lib/terraform/runner_spec.rb index dad4515f..3a693420 100644 --- a/spec/lib/terraform/runner_spec.rb +++ b/spec/lib/terraform/runner_spec.rb @@ -2,11 +2,14 @@ require 'json' RSpec.describe(Terraform::Runner) do + let(:terraform_runner_url) { "https://1.2.3.4:7000" } + let(:embedded_terraform) { ManageIQ::Providers::EmbeddedTerraform::AutomationManager } let(:manager) { FactoryBot.create(:embedded_automation_manager_terraform) } - before(:all) do - ENV["TERRAFORM_RUNNER_TOKEN"] = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlNodWJoYW5naSBTaW5naCIsImlhdCI6MTcwNjAwMDk0M30.46mL8RRxfHI4yveZ2wTsHyF7s2BAiU84aruHBoz2JRQ' + before do + stub_const("ENV", ENV.to_h.merge("TERRAFORM_RUNNER_URL" => terraform_runner_url)) + @hello_world_create_response = JSON.parse(File.read(File.join(__dir__, "runner/data/responses/hello-world-create-success.json"))) @hello_world_retrieve_response = JSON.parse(File.read(File.join(__dir__, "runner/data/responses/hello-world-retrieve-success.json"))) end @@ -17,9 +20,7 @@ describe "is .available" do before do - ENV["TERRAFORM_RUNNER_URL"] = "https://1.2.3.4:7000" - - stub_request(:get, "https://1.2.3.4:7000/ping") + stub_request(:get, "#{terraform_runner_url}/ping") .to_return(:status => 200, :body => {'count' => 0}.to_json) end @@ -42,16 +43,14 @@ def verify_req(req) end before do - ENV["TERRAFORM_RUNNER_URL"] = "https://1.2.3.4:7000" - - create_stub = stub_request(:post, "https://1.2.3.4:7000/api/stack/create") + create_stub = stub_request(:post, "#{terraform_runner_url}/api/stack/create") .with { |req| verify_req(req) } .to_return( :status => 200, :body => @hello_world_create_response.to_json ) - retrieve_stub = stub_request(:post, "https://1.2.3.4:7000/api/stack/retrieve") + retrieve_stub = stub_request(:post, "#{terraform_runner_url}/api/stack/retrieve") .with(:body => hash_including({:stack_id => @hello_world_retrieve_response['stack_id']})) .to_return( :status => 200, @@ -100,9 +99,7 @@ def verify_req(req) retrieve_stub = nil before do - ENV["TERRAFORM_RUNNER_URL"] = "https://1.2.3.4:7000" - - retrieve_stub = stub_request(:post, "https://1.2.3.4:7000/api/stack/retrieve") + retrieve_stub = stub_request(:post, "#{terraform_runner_url}/api/stack/retrieve") .with(:body => hash_including({:stack_id => @hello_world_retrieve_response['stack_id']})) .to_return( :status => 200, @@ -131,9 +128,7 @@ def verify_req(req) cancel_stub = nil before do - ENV["TERRAFORM_RUNNER_URL"] = "https://1.2.3.4:7000" - - create_stub = stub_request(:post, "https://1.2.3.4:7000/api/stack/create") + create_stub = stub_request(:post, "#{terraform_runner_url}/api/stack/create") .with(:body => hash_including({:parameters => [], :cloud_providers => []})) .to_return( :status => 200, @@ -143,7 +138,7 @@ def verify_req(req) cancel_response = @hello_world_create_response.clone cancel_response[:status] = 'CANCELLED' - retrieve_stub = stub_request(:post, "https://1.2.3.4:7000/api/stack/retrieve") + retrieve_stub = stub_request(:post, "#{terraform_runner_url}/api/stack/retrieve") .with(:body => hash_including({:stack_id => @hello_world_retrieve_response['stack_id']})) .to_return( :status => 200, @@ -155,7 +150,7 @@ def verify_req(req) :status => 200, :body => cancel_response.to_json ) - cancel_stub = stub_request(:post, "https://1.2.3.4:7000/api/stack/cancel") + cancel_stub = stub_request(:post, "#{terraform_runner_url}/api/stack/cancel") .with(:body => hash_including({:stack_id => @hello_world_retrieve_response['stack_id']})) .to_return( :status => 200, @@ -245,10 +240,8 @@ def verify_req(req) create_stub = nil before do - ENV["TERRAFORM_RUNNER_URL"] = "https://1.2.3.4:7000" - create_stub = - stub_request(:post, "https://1.2.3.4:7000/api/stack/create") + stub_request(:post, "#{terraform_runner_url}/api/stack/create") .with { |req| verify_req(req) } .to_return( :status => 200, @@ -329,10 +322,8 @@ def verify_req(req) create_stub = nil before do - ENV["TERRAFORM_RUNNER_URL"] = "https://1.2.3.4:7000" - create_stub = - stub_request(:post, "https://1.2.3.4:7000/api/stack/create") + stub_request(:post, "#{terraform_runner_url}/api/stack/create") .with { |req| verify_req(req) } .to_return( :status => 200, @@ -352,4 +343,58 @@ def verify_req(req) end end end + + context '.parse_template_variables hello-world' do + describe '.parse_template_variables input/output vars' do + template_variables_stub = nil + + def verify_req(req) + body = JSON.parse(req.body) + expect(body).to(have_key('templateZipFile')) + end + + before do + hello_world_variables_response = JSON.parse(File.read(File.join(__dir__, "runner/data/responses/hello-world-variables-success.json"))) + + template_variables_stub = stub_request(:post, "#{terraform_runner_url}/api/template/variables") + .with { |req| verify_req(req) } + .to_return( + :status => 200, + :body => hello_world_variables_response.to_json + ) + end + + it "parse input/output params from hello-world terraform template" do + response = Terraform::Runner.parse_template_variables(File.join(__dir__, "runner/data/hello-world")) + expect(template_variables_stub).to(have_been_requested.times(1)) + + template_input_params = response['template_input_params'] + expect(template_input_params.length).to(eq(1)) + expect(template_input_params.first).to be_kind_of(Hash).and include( + "name" => "name", + "label" => "name", + "type" => "string", + "description" => "", + "required" => true, + "secured" => false, + "hidden" => false, + "immutable" => false, + "default" => "World" + ) + + template_output_params = response['template_output_params'] + expect(template_output_params.length).to(eq(1)) + expect(template_output_params.first).to be_kind_of(Hash).and include( + "name" => "greeting", + "label" => "greeting", + "description" => "", + "secured" => false, + "hidden" => false + ) + + terraform_version = response['terraform_version'] + expect(terraform_version).to eq('>= 1.1.0') + end + end + end end diff --git a/spec/models/manageiq/providers/embedded_terraform/automation_manager/configuration_script_source_spec.rb b/spec/models/manageiq/providers/embedded_terraform/automation_manager/configuration_script_source_spec.rb index 18af050a..046e1639 100644 --- a/spec/models/manageiq/providers/embedded_terraform/automation_manager/configuration_script_source_spec.rb +++ b/spec/models/manageiq/providers/embedded_terraform/automation_manager/configuration_script_source_spec.rb @@ -1,4 +1,6 @@ RSpec.describe(ManageIQ::Providers::EmbeddedTerraform::AutomationManager::ConfigurationScriptSource) do + let(:terraform_runner_url) { "https://1.2.3.4:7000" } + context "with a local repo" do let(:manager) { FactoryBot.create(:embedded_automation_manager_terraform) } @@ -15,7 +17,25 @@ let(:repos) { Dir.glob(File.join(repo_dir, "*")) } let(:repo_dir_structure) { %w[hello_world.tf] } + let(:hello_world_vars_response) do + JSON.parse(File.read(File.join(__dir__, "../../../../../lib/terraform/runner/data/responses/hello-world-variables-success.json"))) + end + + def verify_req(req) + body = JSON.parse(req.body) + expect(body).to(have_key('templateZipFile')) + end + before do + stub_const("ENV", ENV.to_h.merge("TERRAFORM_RUNNER_URL" => terraform_runner_url)) + + stub_request(:post, "#{terraform_runner_url}/api/template/variables") + .with { |req| verify_req(req) } + .to_return( + :status => 200, + :body => hello_world_vars_response.to_json + ) + FileUtils.mkdir_p(local_repo) repo = Spec::Support::FakeTerraformRepo.new(local_repo, repo_dir_structure) @@ -85,10 +105,11 @@ def files_in_repository(git_repo_dir) expect(names.first).to(eq("/hello_world_local")) expected_hash = { - "relative_path" => File.dirname(*repo_dir_structure), - "files" => [File.basename(*repo_dir_structure)], - "input_vars" => nil, - "output_vars" => nil + "relative_path" => File.dirname(*repo_dir_structure), + "files" => [File.basename(*repo_dir_structure)], + "input_vars" => hello_world_vars_response['template_input_params'], + "output_vars" => hello_world_vars_response['template_output_params'], + "terraform_version" => hello_world_vars_response['terraform_version'] } expect(payloads.first).to(eq(expected_hash.to_json)) @@ -117,10 +138,11 @@ def files_in_repository(git_repo_dir) expect(names.first).to(eq("templates/hello-world")) expected_hash = { - "relative_path" => File.dirname(*nested_repo_structure), - "files" => [File.basename(*nested_repo_structure)], - "input_vars" => nil, - "output_vars" => nil + "relative_path" => File.dirname(*nested_repo_structure), + "files" => [File.basename(*nested_repo_structure)], + "input_vars" => hello_world_vars_response['template_input_params'], + "output_vars" => hello_world_vars_response['template_output_params'], + "terraform_version" => hello_world_vars_response['terraform_version'] } expect(payloads.first).to(eq(expected_hash.to_json)) @@ -174,17 +196,19 @@ def files_in_repository(git_repo_dir) ) expected_hash1 = { - "relative_path" => File.dirname(multiple_templates_repo_structure.first), - "files" => [File.basename(multiple_templates_repo_structure.first)], - "input_vars" => nil, - "output_vars" => nil + "relative_path" => File.dirname(multiple_templates_repo_structure.first), + "files" => [File.basename(multiple_templates_repo_structure.first)], + "input_vars" => hello_world_vars_response['template_input_params'], + "output_vars" => hello_world_vars_response['template_output_params'], + "terraform_version" => hello_world_vars_response['terraform_version'] } expected_hash2 = { - "relative_path" => File.dirname(multiple_templates_repo_structure.second), - "files" => [File.basename(multiple_templates_repo_structure.second)], - "input_vars" => nil, - "output_vars" => nil + "relative_path" => File.dirname(multiple_templates_repo_structure.second), + "files" => [File.basename(multiple_templates_repo_structure.second)], + "input_vars" => hello_world_vars_response['template_input_params'], + "output_vars" => hello_world_vars_response['template_output_params'], + "terraform_version" => hello_world_vars_response['terraform_version'] } expect(payloads).to(match_array([expected_hash1.to_json, expected_hash2.to_json])) @@ -282,10 +306,11 @@ def files_in_repository(git_repo_dir) expect(names.first).to(eq("/hello_world_local")) expected_hash = { - "relative_path" => File.dirname(*repo_dir_structure), - "files" => [File.basename(*repo_dir_structure)], - "input_vars" => nil, - "output_vars" => nil + "relative_path" => File.dirname(*repo_dir_structure), + "files" => [File.basename(*repo_dir_structure)], + "input_vars" => hello_world_vars_response['template_input_params'], + "output_vars" => hello_world_vars_response['template_output_params'], + "terraform_version" => hello_world_vars_response['terraform_version'] } expect(payloads.first).to(eq(expected_hash.to_json))