diff --git a/app/models/manageiq/providers/embedded_terraform/automation_manager/job.rb b/app/models/manageiq/providers/embedded_terraform/automation_manager/job.rb new file mode 100644 index 00000000..d8823d7c --- /dev/null +++ b/app/models/manageiq/providers/embedded_terraform/automation_manager/job.rb @@ -0,0 +1,129 @@ +class ManageIQ::Providers::EmbeddedTerraform::AutomationManager::Job < Job + def self.create_job(template, env_vars, input_vars, credentials, poll_interval: 1.minute) + super( + :template_id => template.id, + :env_vars => env_vars, + :input_vars => input_vars, + :credentials => credentials, + :poll_interval => poll_interval, + ) + end + + def start + queue_signal(:pre_execute) + end + + def pre_execute + checkout_git_repository + signal(:execute) + end + + def execute + template_path = File.join(options[:git_checkout_tempdir], template_relative_path) + + response = Terraform::Runner.run( + options[:input_vars], + template_path, + :credentials => options[:credentials], + :env_vars => options[:env_vars] + ) + + options[:terraform_stack_id] = response.stack_id + save! + + queue_poll_runner + end + + def poll_runner + if running? + queue_poll_runner + else + signal(:post_execute) + end + end + + def post_execute + cleanup_git_repository + + success? ? queue_signal(:finish, message, status) : abort_job("Failed to run template", "error") + end + + alias initializing dispatch_start + alias finish process_finished + alias abort_job process_abort + alias cancel process_cancel + alias error process_error + + protected + + def running? + stack_response&.running? + end + + def success? + stack_response&.response&.status == "SUCCESS" + end + + def load_transitions + self.state ||= 'initialize' + + { + :initializing => {'initialize' => 'waiting_to_start'}, + :start => {'waiting_to_start' => 'pre_execute'}, + :pre_execute => {'pre_execute' => 'execute'}, + :execute => {'execute' => 'running'}, + :poll_runner => {'running' => 'running'}, + :post_execute => {'running' => 'post_execute'}, + :finish => {'*' => 'finished'}, + :abort_job => {'*' => 'aborting'}, + :cancel => {'*' => 'canceling'}, + :error => {'*' => '*'} + } + end + + def poll_interval + options.fetch(:poll_interval, 1.minute).to_i + end + + private + + def template + @template ||= self.class.module_parent::Template.find(options[:template_id]) + end + + def template_relative_path + JSON.parse(template.payload)["relative_path"] + end + + def stack_response + return if options[:terraform_stack_id].nil? + + @stack_response ||= Terraform::Runner::ResponseAsync.new(options[:terraform_stack_id]) + end + + def configuration_script_source + @configuration_script_source ||= template.configuration_script_source + end + + def queue_poll_runner + queue_signal(:poll_runner, :deliver_on => Time.now.utc + poll_interval) + end + + def checkout_git_repository + options[:git_checkout_tempdir] = Dir.mktmpdir("embedded-terraform-runner-git") + save! + + _log.info("Checking out git repository to #{options[:git_checkout_tempdir].inspect}...") + configuration_script_source.checkout_git_repository(options[:git_checkout_tempdir]) + rescue MiqException::MiqUnreachableError => err + miq_task.job.timeout! + raise "Failed to connect with [#{err.class}: #{err}], job aborted" + end + + def cleanup_git_repository + return unless options[:git_checkout_tempdir] + + _log.info("Cleaning up git repository checkout at #{options[:git_checkout_tempdir].inspect}...") + FileUtils.rm_rf(options[:git_checkout_tempdir]) + end +end diff --git a/app/models/manageiq/providers/embedded_terraform/automation_manager/stack.rb b/app/models/manageiq/providers/embedded_terraform/automation_manager/stack.rb new file mode 100644 index 00000000..338cacf0 --- /dev/null +++ b/app/models/manageiq/providers/embedded_terraform/automation_manager/stack.rb @@ -0,0 +1,44 @@ +class ManageIQ::Providers::EmbeddedTerraform::AutomationManager::Stack < ManageIQ::Providers::EmbeddedAutomationManager::OrchestrationStack + belongs_to :ext_management_system, :foreign_key => :ems_id, :class_name => "ManageIQ::Providers::EmbeddedTerraform::AutomationManager", :inverse_of => false + belongs_to :configuration_script_payload, :foreign_key => :configuration_script_base_id, :class_name => "ManageIQ::Providers::EmbeddedTerraform::AutomationManager::Template", :inverse_of => :stacks + belongs_to :miq_task, :foreign_key => :ems_ref, :inverse_of => false + + class << self + alias create_job create_stack + alias raw_create_job raw_create_stack + + def create_stack(terraform_template, options = {}) + authentications = collect_authentications(terraform_template.manager, options) + + job = raw_create_stack(terraform_template, options) + + miq_task = job&.miq_task + + create!( + :name => terraform_template.name, + :ext_management_system => terraform_template.manager, + :verbosity => options[:verbosity].to_i, + :authentications => authentications, + :configuration_script_payload => terraform_template, + :miq_task => miq_task, + :status => miq_task&.state, + :start_time => miq_task&.started_on + ) + end + + def raw_create_stack(terraform_template, options = {}) + terraform_template.run(options) + rescue => err + _log.error("Failed to create job from template(#{terraform_template.name}), error: #{err}") + raise MiqException::MiqOrchestrationProvisionError, err.to_s, err.backtrace + end + + private + + def collect_authentications(manager, options) + credential_ids = options[:credential] || [] + + manager.credentials.where(:id => credential_ids) + end + 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 7ccad343..d9161156 100644 --- a/app/models/manageiq/providers/embedded_terraform/automation_manager/template.rb +++ b/app/models/manageiq/providers/embedded_terraform/automation_manager/template.rb @@ -1,6 +1,10 @@ class ManageIQ::Providers::EmbeddedTerraform::AutomationManager::Template < ManageIQ::Providers::EmbeddedAutomationManager::ConfigurationScriptPayload - has_many :jobs, :class_name => 'OrchestrationStack', :foreign_key => :configuration_script_base_id + 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) || {} + credentials = vars.delete(:credential) || [] + + self.class.module_parent::Job.create_job(self, env_vars, vars, credentials).tap(&:signal_start) end end diff --git a/lib/terraform/runner/response_async.rb b/lib/terraform/runner/response_async.rb index f8b3c9cc..3f6134c1 100644 --- a/lib/terraform/runner/response_async.rb +++ b/lib/terraform/runner/response_async.rb @@ -3,6 +3,8 @@ class Runner class ResponseAsync include Vmdb::Logging + attr_reader :stack_id + # Response object designed for holding full response from terraform-runner stack job # # @param stack_id [String] terraform-runner stack job - stack_id diff --git a/spec/factories/configuration_script_payload.rb b/spec/factories/configuration_script_payload.rb new file mode 100644 index 00000000..5335a531 --- /dev/null +++ b/spec/factories/configuration_script_payload.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :terraform_template, + :class => "ManageIQ::Providers::EmbeddedTerraform::AutomationManager::Template", + :parent => :configuration_script_payload +end diff --git a/spec/models/manageiq/providers/embedded_terraform/automation_manager/job_spec.rb b/spec/models/manageiq/providers/embedded_terraform/automation_manager/job_spec.rb new file mode 100644 index 00000000..e019a238 --- /dev/null +++ b/spec/models/manageiq/providers/embedded_terraform/automation_manager/job_spec.rb @@ -0,0 +1,141 @@ +RSpec.describe ManageIQ::Providers::EmbeddedTerraform::AutomationManager::Job do + let(:template) { FactoryBot.create(:terraform_template) } + let(:job) { described_class.create_job(template, env_vars, input_vars, credentials).tap { |job| job.state = state} } + let(:state) { "waiting_to_start" } + let(:env_vars) { {} } + let(:input_vars) { {} } + let(:credentials) { [] } + + describe ".create_job" do + it "create a job" do + expect(described_class.create_job(template, env_vars, input_vars, credentials)).to have_attributes( + :type => "ManageIQ::Providers::EmbeddedTerraform::AutomationManager::Job", + :options => { + :template_id => template.id, + :env_vars => env_vars, + :input_vars => input_vars, + :credentials => credentials, + :poll_interval => 60 + } + ) + end + end + + describe "#signal" do + %w[start pre_execute execute poll_runner post_execute finish abort_job cancel error].each do |signal| + shared_examples_for "allows #{signal} signal" do + it signal.to_s do + expect(job).to receive(signal.to_sym) + job.signal(signal.to_sym) + end + end + end + + %w[start pre_execute execute poll_runner post_execute finish abort_job cancel error].each do |signal| + shared_examples_for "doesn't allow #{signal} signal" do + it signal.to_s do + expect { job.signal(signal.to_sym) }.to raise_error(RuntimeError, /#{signal} is not permitted at state #{job.state}/) + end + end + end + + context "waiting_to_start" do + let(:state) { "waiting_to_start" } + + it_behaves_like "allows start signal" + it_behaves_like "doesn't allow pre_execute signal" + it_behaves_like "doesn't allow execute signal" + it_behaves_like "doesn't allow poll_runner signal" + it_behaves_like "doesn't allow post_execute signal" + it_behaves_like "allows finish signal" + it_behaves_like "allows abort_job signal" + it_behaves_like "allows cancel signal" + it_behaves_like "allows error signal" + end + + context "pre_execute" do + let(:state) { "pre_execute" } + + it_behaves_like "doesn't allow start signal" + it_behaves_like "allows pre_execute signal" + it_behaves_like "doesn't allow execute signal" + it_behaves_like "doesn't allow poll_runner signal" + it_behaves_like "doesn't allow post_execute signal" + it_behaves_like "allows finish signal" + it_behaves_like "allows abort_job signal" + it_behaves_like "allows cancel signal" + it_behaves_like "allows error signal" + end + + context "running" do + let(:state) { "running" } + + it_behaves_like "doesn't allow start signal" + it_behaves_like "doesn't allow pre_execute signal" + it_behaves_like "doesn't allow execute signal" + it_behaves_like "allows poll_runner signal" + it_behaves_like "allows post_execute signal" + it_behaves_like "allows finish signal" + it_behaves_like "allows abort_job signal" + it_behaves_like "allows cancel signal" + it_behaves_like "allows error signal" + end + + context "post_execute" do + let(:state) { "post_execute" } + + it_behaves_like "doesn't allow start signal" + it_behaves_like "doesn't allow pre_execute signal" + it_behaves_like "doesn't allow execute signal" + it_behaves_like "doesn't allow poll_runner signal" + it_behaves_like "doesn't allow post_execute signal" + it_behaves_like "allows finish signal" + it_behaves_like "allows abort_job signal" + it_behaves_like "allows cancel signal" + it_behaves_like "allows error signal" + end + + context "finished" do + let(:state) { "finished" } + + it_behaves_like "doesn't allow start signal" + it_behaves_like "doesn't allow pre_execute signal" + it_behaves_like "doesn't allow execute signal" + it_behaves_like "doesn't allow poll_runner signal" + it_behaves_like "doesn't allow post_execute signal" + it_behaves_like "allows finish signal" + it_behaves_like "allows abort_job signal" + it_behaves_like "allows cancel signal" + it_behaves_like "allows error signal" + end + end + + describe "#start" do + it "moves to state pre_execute" do + job.signal(:start) + expect(job.reload.state).to eq("pre_execute") + end + end + + describe "#poll_runner" do + let(:state) { "running" } + + context "still running" do + before { expect(job).to receive(:running?).and_return(true) } + + it "requeues poll_runner" do + job.signal(:poll_runner) + expect(job.reload.state).to eq("running") + end + end + + context "completed" do + before { expect(job).to receive(:running?).and_return(false) } + + it "moves to state finished" do + job.signal(:poll_runner) + expect(job.reload.state).to eq("finished") + end + end + end +end diff --git a/spec/models/manageiq/providers/embedded_terraform/automation_manager/template_spec.rb b/spec/models/manageiq/providers/embedded_terraform/automation_manager/template_spec.rb new file mode 100644 index 00000000..fedabb8f --- /dev/null +++ b/spec/models/manageiq/providers/embedded_terraform/automation_manager/template_spec.rb @@ -0,0 +1,10 @@ +RSpec.describe ManageIQ::Providers::EmbeddedTerraform::AutomationManager::Template do + let(:template) { FactoryBot.create(:terraform_template) } + + describe "#run" do + it "creates a Job" do + job = template.run + expect(job).to be_a(ManageIQ::Providers::EmbeddedTerraform::AutomationManager::Job) + end + end +end