diff --git a/app/models/manageiq/providers/container_manager.rb b/app/models/manageiq/providers/container_manager.rb index 8f346f523f3..ab70736acae 100644 --- a/app/models/manageiq/providers/container_manager.rb +++ b/app/models/manageiq/providers/container_manager.rb @@ -1,5 +1,7 @@ module ManageIQ::Providers class ContainerManager < BaseManager + require_nested :OrchestrationStack + include AvailabilityMixin include HasMonitoringManagerMixin include SupportsFeatureMixin diff --git a/app/models/manageiq/providers/container_manager/orchestration_stack.rb b/app/models/manageiq/providers/container_manager/orchestration_stack.rb new file mode 100644 index 00000000000..15f5b5fa236 --- /dev/null +++ b/app/models/manageiq/providers/container_manager/orchestration_stack.rb @@ -0,0 +1,65 @@ +class ManageIQ::Providers::ContainerManager::OrchestrationStack < ::OrchestrationStack + belongs_to :ext_management_system, :foreign_key => :ems_id, :class_name => "ManageIQ::Providers::ContainerManager" + belongs_to :container_template, :foreign_key => :orchestration_template_id, :class_name => "ContainerTemplate" + + def self.create_stack(container_template, params, project_name) + new(:name => container_template.name, + :ext_management_system => container_template.ext_management_system, + :container_template => container_template).tap do |stack| + stack.send(:add_provider_objects, raw_create_stack(container_template, params, project_name)) + stack.save! + end + end + + def self.raw_create_stack(container_template, params, project_name) + container_template.instantiate(params, project_name) + rescue => err + _log.error("Failed to provision from container template [#{container_template.name}], error: [#{err}]") + raise MiqException::MiqOrchestrationProvisionError, err.to_s, err.backtrace + end + + def self.status_class + "#{name}::Status".constantize + end + + def raw_status + failed = resources.any? { |obj| obj.resource_status == 'failed' } + if failed + update_attributes(:status => 'failed') + return self.class.status_class.new('failed', nil) + end + + done = resources.all? do |obj| + miq_class = obj.resource_category + miq_obj = miq_class.constantize.find_by(:ems_ref => obj.ems_ref) if miq_class + obj.update_attributes(:resource_status => 'succeeded') if miq_obj + miq_class.nil? || miq_obj + end + + update_attributes(:status => 'succeeded') if done + message = done ? "completed" : "in progress" + + self.class.status_class.new(message, nil) + end + + def add_provider_objects(objects) + self.resources = objects.collect { |object| add_provider_object(object) } + end + private :add_provider_objects + + def add_provider_object(object) + options = { + :name => object[:metadata][:name], + :physical_resource => object[:metadata][:namespace], + :ems_ref => object[:metadata][:uid], + :start_time => object[:metadata][:creationTimestamp], + :logical_resource => object[:kind], + :resource_category => object[:miq_class], + :description => object[:apiVersion], + :resource_status => 'creating' + } + options[:resource_status] = 'failed' if object[:kind].blank? + OrchestrationStackResource.new(options) + end + private :add_provider_object +end diff --git a/app/models/service_container_template.rb b/app/models/service_container_template.rb new file mode 100644 index 00000000000..ae419de4a99 --- /dev/null +++ b/app/models/service_container_template.rb @@ -0,0 +1,113 @@ +class ServiceContainerTemplate < ServiceGeneric + delegate :container_template, :container_manager, :to => :service_template, :allow_nil => true + + # A chance for taking options from automate script to override options from a service dialog + def preprocess(action, new_options = {}) + return unless action == ResourceAction::PROVISION + + unless new_options.blank? + _log.info("Override with new options:") + $log.log_hashes(new_options) + end + + save_action_options(action, new_options) + end + + def execute(action) + return unless action == ResourceAction::PROVISION + + opts = get_action_options(action) + + _log.info("Container template provisioning with options:") + $log.log_hashes(opts) + + params = process_parameters(opts[:parameters]) + stack_klass = "#{container_manager.class.name}::OrchestrationStack".constantize + new_stack = stack_klass.create_stack(container_template, params, opts[:container_project_name]) + _log.info("Container provisioning with template ID: [#{id}] name:[#{name}] was initiated.") + + add_resource!(new_stack, :name => action) + end + + def check_completed(action) + return [true, 'not supported'] unless action == ResourceAction::PROVISION + + status, reason = stack.raw_status.normalized_status + done = status != 'transient' + message = status == 'create_complete' ? nil : reason + [done, message] + end + + def refresh(action) + end + + def check_refreshed(_action) + [true, nil] + end + + def on_error(action) + _log.info("on_error called for service: [#{name}] action: [#{action}]") + end + + def stack + service_resources.find_by(:name => ResourceAction::PROVISION, :resource_type => 'OrchestrationStack').try(:resource) + end + + private + + def process_parameters(inputs) + params = container_template.container_template_parameters.to_a + inputs.each do |key, value| + match = params.find { |p| p.name == key.to_s } + match.value = value if match + end + params + end + + def get_action_options(action) + options[action_option_key(action)].deep_dup + end + + def save_action_options(action, overrides) + return unless action == ResourceAction::PROVISION + + action_options = { + :container_project_name => project_name(overrides), + :parameters => parameters_from_dialog.with_indifferent_access.merge(overrides) + } + + options[action_option_key(action)] = action_options + save! + end + + def action_option_key(action) + "#{action.downcase}_options".to_sym + end + + def parameters_from_dialog + params = + options[:dialog].each_with_object({}) do |(attr, val), obj| + var_key = attr.sub(/dialog_param_/, '') + obj[var_key] = val unless var_key == attr + end + + params.blank? ? {} : params + end + + def project_name(overrides) + # :dialog option should specify the project name, either an existing project or a new project name + dialog_options = options[:dialog] + existing_name = overrides.delete(:existing_project_name) || dialog_options['dialog_existing_project_name'] + new_project_name = overrides.delete(:new_project_name) || dialog_options['dialog_new_project_name'] + + create_project(new_project_name) if new_project_name + project_name = new_project_name || existing_name + + raise _("A project is required for the container template provisioning") unless project_name + project_name + end + + def create_project(name) + container_manager.create_project(:metadata => {:name => name}) + end +end diff --git a/spec/factories/service.rb b/spec/factories/service.rb index 5ae751ee6da..c84fdef58af 100644 --- a/spec/factories/service.rb +++ b/spec/factories/service.rb @@ -11,4 +11,7 @@ factory :service_ansible_playbook, :class => :ServiceAnsiblePlaybook, :parent => :service do end + + factory :service_container_template, :class => :ServiceContainerTemplate, :parent => :service do + end end diff --git a/spec/models/service_container_template_spec.rb b/spec/models/service_container_template_spec.rb new file mode 100644 index 00000000000..1be608e46a9 --- /dev/null +++ b/spec/models/service_container_template_spec.rb @@ -0,0 +1,152 @@ +describe(ServiceContainerTemplate) do + let(:action) { ResourceAction::PROVISION } + let(:stack_status) { double("ManageIQ::Providers::Openshift::ContainerManager::OrchestrationStack::Status") } + let(:stack) do + double("ManageIQ::Providers::Openshift::ContainerManager::OrchestrationStack", :resources => [created_object], :raw_status => stack_status) + end + + let(:ems) do + FactoryGirl.create(:ems_openshift).tap do |ems| + allow(ems).to receive(:create_project) + end + end + + let(:service) do + FactoryGirl.create(:service_container_template, :options => config_info_options.merge(dialog_options)).tap do |svc| + allow(svc).to receive(:container_manager).and_return(ems) + end + end + + let(:service_with_new_project) do + FactoryGirl.create(:service_container_template, :options => config_info_options.merge(dialog_options_with_new_project)).tap do |svc| + allow(svc).to receive(:container_manager).and_return(ems) + end + end + + let(:loaded_service) do + service_template = FactoryGirl.create(:service_template_container_template).tap do |st| + allow(st).to receive(:container_manager).and_return(ems) + end + + FactoryGirl.create(:service_container_template, + :options => provision_options.merge(config_info_options), + :service_template => service_template).tap do |svc| + allow(svc).to receive(:container_template).and_return(container_template) + allow(svc).to receive(:stack).and_return(stack) + end + end + + let(:dialog_options) do + { + :dialog => { + 'dialog_existing_project_name' => 'old_project', + 'dialog_param_var1' => 'value1', + 'dialog_param_var2' => 'value2' + } + } + end + + let(:dialog_options_with_new_project) do + { + :dialog => { + 'dialog_existing_project_name' => 'old_project', + 'dialog_new_project_name' => 'new_project', + 'dialog_param_var1' => 'value1', + 'dialog_param_var2' => 'value2' + } + } + end + + let(:config_info_options) do + { + :config_info => { + :provision => { + :dialog_id => 2, + :container_template => container_template + } + } + } + end + + let(:override_options) { {:new_project_name => 'override_project', :var1 => 'new_val1'} } + + let(:provision_options) do + { + :provision_options => { + :container_project_name => 'my-project', + :parameters => {'var1' => 'value1', 'var2' => 'value2'} + } + } + end + + let(:ctp1) { FactoryGirl.create(:container_template_parameter, :name => 'var1', :value => 'p1', :required => true) } + let(:ctp2) { FactoryGirl.create(:container_template_parameter, :name => 'var2', :value => 'p2', :required => true) } + let(:ctp3) { FactoryGirl.create(:container_template_parameter, :name => 'var3', :value => 'p3', :required => false) } + let(:container_template) do + FactoryGirl.create(:container_template, :ems_id => ems.id).tap do |ct| + ct.container_template_parameters = [ctp1, ctp2, ctp3] + end + end + + let(:created_object) { FactoryGirl.create(:orchestration_stack_resource, :name => 'my-example', :resource_category => 'ContainerRoute') } + let(:object_hash) { {:apiVersion => "v1", :kind => "Route", :metadata => {:name => "dotnet-example"}} } + + describe '#preprocess' do + it 'prepares job options from dialog' do + expect(ems).not_to receive(:create_project) + service.preprocess(action) + expect(service.options[:provision_options]).to have_attributes( + :container_project_name => 'old_project', + :parameters => {"var1" => "value1", "var2" => "value2"} + ) + end + + it 'honors new project name more than existing project name' do + expect(ems).to receive(:create_project) + service_with_new_project.preprocess(action) + expect(service_with_new_project.options[:provision_options]).to have_attributes( + :container_project_name => 'new_project', + :parameters => {"var1" => "value1", "var2" => "value2"} + ) + end + + it 'prepares job options combined from dialog and overrides' do + expect(ems).to receive(:create_project) + service_with_new_project.preprocess(action, override_options) + expect(service_with_new_project.options[:provision_options]).to have_attributes( + :container_project_name => 'override_project', + :parameters => {'var1' => 'new_val1', 'var2' => 'value2'} + ) + end + end + + describe '#execute' do + it 'Provisions with a container template' do + expect(container_template).to receive(:instantiate) do |params, project_name| + expect(project_name).to eq(provision_options.fetch_path(:provision_options, :container_project_name)) + expect(params).to match_array([ctp1, ctp2, ctp3]) + expect(ctp1.value).to eq(provision_options.fetch_path(:provision_options, :parameters, ctp1.name)) + expect(ctp2.value).to eq(provision_options.fetch_path(:provision_options, :parameters, ctp2.name)) + expect(ctp3.value).to eq(ctp3.value) + [object_hash] + end + loaded_service.execute(action) + end + end + + describe '#check_completed' do + it 'created container object ends in VMDB' do + allow(stack_status).to receive(:normalized_status).and_return(%w(create_complete completed)) + expect(loaded_service.check_completed(action)).to eq([true, nil]) + end + + it 'created container object not ends in VMDB yet' do + allow(stack_status).to receive(:normalized_status).and_return(['transient', 'in progress']) + expect(loaded_service.check_completed(action)).to eq([false, 'in progress']) + end + end + + describe '#check_refreshed' do + it { expect(loaded_service.check_refreshed(action)).to eq([true, nil]) } + end +end