From ad9c471a9a4a78041f9c3a9546eccbbaa03e87b6 Mon Sep 17 00:00:00 2001 From: Raphael Date: Fri, 26 Jun 2026 16:10:15 +0200 Subject: [PATCH 1/2] feat: flow stream will provide module definitions for actions --- app/grpc/flow_handler.rb | 37 ++++++++---- app/models/module_configuration_definition.rb | 7 +++ .../runtimes/grpc/modules/update_service.rb | 21 ++++++- spec/grpc/flow_handler_spec.rb | 58 +++++++++++++++++++ .../module_configuration_definition_spec.rb | 11 ++++ .../grpc/modules/update_service_spec.rb | 46 +++++++++++++++ 6 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 spec/grpc/flow_handler_spec.rb create mode 100644 spec/services/runtimes/grpc/modules/update_service_spec.rb diff --git a/app/grpc/flow_handler.rb b/app/grpc/flow_handler.rb index 4861a86d..40abee55 100644 --- a/app/grpc/flow_handler.rb +++ b/app/grpc/flow_handler.rb @@ -12,6 +12,7 @@ def self.update_runtime(runtime) :namespace_project, module_configurations: { module_configuration_definition: :runtime_module } ) + runtime_modules = runtime.runtime_modules.includes(:module_configuration_definitions) flows = [] assignments.each do |assignment| @@ -29,7 +30,7 @@ def self.update_runtime(runtime) runtime.id ) - grouped_module_configurations(assignments).each do |module_configuration| + grouped_module_configurations(assignments, runtime_modules).each do |module_configuration| send_update( Tucana::Sagittarius::FlowResponse.new( module_configurations: module_configuration @@ -48,14 +49,18 @@ def self.update_started(runtime_id) update_runtime(runtime) end - def self.grouped_module_configurations(assignments) + def self.grouped_module_configurations(assignments, runtime_modules) grouped_entries = assignments.flat_map do |assignment| - assignment.module_configurations.map do |configuration| - [ - configuration.module_configuration_definition.runtime_module.identifier, - assignment, - configuration - ] + saved_configurations = assignment.module_configurations.index_by(&:module_configuration_definition_id) + + runtime_modules.flat_map do |runtime_module| + runtime_module.module_configuration_definitions.map do |definition| + [ + runtime_module.identifier, + assignment, + saved_configurations[definition.id] || definition + ] + end end end.group_by(&:first) @@ -81,8 +86,20 @@ def self.grouped_project_configurations(entries) def self.grpc_module_configurations(entries) entries.map(&:last) - .sort_by { |configuration| configuration.module_configuration_definition.identifier } - .map(&:to_grpc) + .sort_by { |configuration| module_configuration_identifier(configuration) } + .map { |configuration| module_configuration_to_grpc(configuration) } + end + + def self.module_configuration_identifier(configuration) + return configuration.identifier if configuration.is_a?(ModuleConfigurationDefinition) + + configuration.module_configuration_definition.identifier + end + + def self.module_configuration_to_grpc(configuration) + return configuration.to_default_grpc if configuration.is_a?(ModuleConfigurationDefinition) + + configuration.to_grpc end def self.encoders = { update: ->(grpc_object) { Tucana::Sagittarius::FlowResponse.encode(grpc_object) } } diff --git a/app/models/module_configuration_definition.rb b/app/models/module_configuration_definition.rb index cf6b9a62..56675a16 100644 --- a/app/models/module_configuration_definition.rb +++ b/app/models/module_configuration_definition.rb @@ -20,4 +20,11 @@ class ModuleConfigurationDefinition < ApplicationRecord validates :type, presence: true, length: { maximum: 8192 } validates :optional, inclusion: { in: [true, false] } validates :hidden, inclusion: { in: [true, false] } + + def to_default_grpc + Tucana::Shared::ModuleConfiguration.new( + identifier: identifier, + value: Tucana::Shared::Value.from_ruby(default_value) + ) + end end diff --git a/app/services/runtimes/grpc/modules/update_service.rb b/app/services/runtimes/grpc/modules/update_service.rb index 3a3ae710..ee16e776 100644 --- a/app/services/runtimes/grpc/modules/update_service.rb +++ b/app/services/runtimes/grpc/modules/update_service.rb @@ -25,7 +25,7 @@ def initialize(current_runtime, modules, definition_update_services: DEFAULT_DEF end def execute - transactional do |t| + response = transactional do |t| lock_existing_modules(t) module_records = update_modules(t) @@ -35,12 +35,16 @@ def execute update_module_definitions(module_records.payload, t) update_definition_services(module_records.payload, t) - UpdateRuntimeCompatibilityJob.perform_later({ runtime_id: current_runtime.id }) - logger.info(message: 'Updated modules for runtime', runtime_id: current_runtime.id) ServiceResponse.success(message: 'Updated modules', payload: modules) end + + return response if response.error? + + update_runtime_compatibility + FlowHandler.update_runtime(current_runtime) + response end protected @@ -161,6 +165,17 @@ def build_definition_update_service(service, definitions, runtime_module) update_runtime_compatibility: false ) end + + def update_runtime_compatibility + current_runtime.project_assignments.find_each do |assignment| + response = Runtimes::CheckRuntimeCompatibilityService.new( + assignment.runtime, + assignment.namespace_project + ).execute + + assignment.update!(compatible: response.success?) + end + end end end end diff --git a/spec/grpc/flow_handler_spec.rb b/spec/grpc/flow_handler_spec.rb new file mode 100644 index 00000000..5f28565b --- /dev/null +++ b/spec/grpc/flow_handler_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe FlowHandler do + describe '.grouped_module_configurations' do + let(:runtime) { create(:runtime) } + let(:project) { create(:namespace_project) } + let(:assignment) do + create(:namespace_project_runtime_assignment, + namespace_project: project, + runtime: runtime, + compatible: true) + end + let(:runtime_module) { create(:runtime_module, runtime: runtime, identifier: 'example-action') } + let!(:saved_definition) do + create(:module_configuration_definition, + runtime_module: runtime_module, + identifier: 'EXAMPLE_CONFIG', + default_value: 'default') + end + let(:default_definition) do + create(:module_configuration_definition, + runtime_module: runtime_module, + identifier: 'SECOND_CONFIG', + default_value: 'second-default') + end + + before do + create(:module_configuration, + namespace_project_runtime_assignment: assignment, + module_configuration_definition: saved_definition, + value: 'saved') + end + + it 'uses saved values and falls back to definition defaults for missing project values' do + default_definition + + module_configurations = described_class.grouped_module_configurations([assignment], [runtime_module]) + + expect(module_configurations.length).to eq(1) + expect(module_configurations.first.module_identifier).to eq('example-action') + + project_configurations = module_configurations.first.module_configurations.sole + expect(project_configurations.project_id).to eq(project.id) + expect( + project_configurations.module_configurations.map do |configuration| + [configuration.identifier, configuration.value.to_ruby(true)] + end + ).to eq( + [ + %w[EXAMPLE_CONFIG saved], + %w[SECOND_CONFIG second-default] + ] + ) + end + end +end diff --git a/spec/models/module_configuration_definition_spec.rb b/spec/models/module_configuration_definition_spec.rb index af9584f5..6d4ab063 100644 --- a/spec/models/module_configuration_definition_spec.rb +++ b/spec/models/module_configuration_definition_spec.rb @@ -29,4 +29,15 @@ it { is_expected.to allow_values(true, false).for(:optional) } it { is_expected.to allow_values(true, false).for(:hidden) } end + + describe '#to_default_grpc' do + it 'serializes the identifier and default value as a module configuration' do + configuration_definition.update!(identifier: 'apiKey', default_value: 'default-secret') + + grpc_configuration = configuration_definition.to_default_grpc + + expect(grpc_configuration.identifier).to eq('apiKey') + expect(grpc_configuration.value.to_ruby(true)).to eq('default-secret') + end + end end diff --git a/spec/services/runtimes/grpc/modules/update_service_spec.rb b/spec/services/runtimes/grpc/modules/update_service_spec.rb new file mode 100644 index 00000000..6ef82738 --- /dev/null +++ b/spec/services/runtimes/grpc/modules/update_service_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Runtimes::Grpc::Modules::UpdateService do + describe '#execute' do + let(:runtime) { create(:runtime) } + let(:project) { create(:namespace_project) } + let!(:assignment) do + create(:namespace_project_runtime_assignment, + runtime: runtime, + namespace_project: project, + compatible: false) + end + let(:compatibility_service) { instance_double(Runtimes::CheckRuntimeCompatibilityService, execute: compatibility_response) } + let(:compatibility_response) { ServiceResponse.success } + + before do + allow(FlowHandler).to receive(:update_runtime) + allow(Runtimes::CheckRuntimeCompatibilityService).to receive(:new).and_return(compatibility_service) + end + + it 'updates assignment compatibility before updating the runtime flow stream' do + response = described_class.new(runtime, []).execute + + expect(response).to be_success + expect(Runtimes::CheckRuntimeCompatibilityService).to have_received(:new).with(runtime, project) + expect(assignment.reload.compatible).to be(true) + expect(FlowHandler).to have_received(:update_runtime).with(runtime) + end + + context 'when the runtime is not compatible with a project assignment' do + let(:compatibility_response) { ServiceResponse.error(message: 'Not compatible', error_code: :missing_definition) } + + it 'marks the assignment as incompatible before updating the runtime flow stream' do + assignment.update!(compatible: true) + + response = described_class.new(runtime, []).execute + + expect(response).to be_success + expect(assignment.reload.compatible).to be(false) + expect(FlowHandler).to have_received(:update_runtime).with(runtime) + end + end + end +end From 47e13e926c659eb6b69c3657b85fe907f164a90f Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 2 Jul 2026 17:19:27 +0200 Subject: [PATCH 2/2] fix: perform update runtime job later --- .../runtimes/grpc/modules/update_service.rb | 21 ++---------- .../grpc/modules/update_service_spec.rb | 32 ++----------------- 2 files changed, 6 insertions(+), 47 deletions(-) diff --git a/app/services/runtimes/grpc/modules/update_service.rb b/app/services/runtimes/grpc/modules/update_service.rb index ee16e776..3a3ae710 100644 --- a/app/services/runtimes/grpc/modules/update_service.rb +++ b/app/services/runtimes/grpc/modules/update_service.rb @@ -25,7 +25,7 @@ def initialize(current_runtime, modules, definition_update_services: DEFAULT_DEF end def execute - response = transactional do |t| + transactional do |t| lock_existing_modules(t) module_records = update_modules(t) @@ -35,16 +35,12 @@ def execute update_module_definitions(module_records.payload, t) update_definition_services(module_records.payload, t) + UpdateRuntimeCompatibilityJob.perform_later({ runtime_id: current_runtime.id }) + logger.info(message: 'Updated modules for runtime', runtime_id: current_runtime.id) ServiceResponse.success(message: 'Updated modules', payload: modules) end - - return response if response.error? - - update_runtime_compatibility - FlowHandler.update_runtime(current_runtime) - response end protected @@ -165,17 +161,6 @@ def build_definition_update_service(service, definitions, runtime_module) update_runtime_compatibility: false ) end - - def update_runtime_compatibility - current_runtime.project_assignments.find_each do |assignment| - response = Runtimes::CheckRuntimeCompatibilityService.new( - assignment.runtime, - assignment.namespace_project - ).execute - - assignment.update!(compatible: response.success?) - end - end end end end diff --git a/spec/services/runtimes/grpc/modules/update_service_spec.rb b/spec/services/runtimes/grpc/modules/update_service_spec.rb index 6ef82738..8eb5f3dc 100644 --- a/spec/services/runtimes/grpc/modules/update_service_spec.rb +++ b/spec/services/runtimes/grpc/modules/update_service_spec.rb @@ -5,42 +5,16 @@ RSpec.describe Runtimes::Grpc::Modules::UpdateService do describe '#execute' do let(:runtime) { create(:runtime) } - let(:project) { create(:namespace_project) } - let!(:assignment) do - create(:namespace_project_runtime_assignment, - runtime: runtime, - namespace_project: project, - compatible: false) - end - let(:compatibility_service) { instance_double(Runtimes::CheckRuntimeCompatibilityService, execute: compatibility_response) } - let(:compatibility_response) { ServiceResponse.success } before do - allow(FlowHandler).to receive(:update_runtime) - allow(Runtimes::CheckRuntimeCompatibilityService).to receive(:new).and_return(compatibility_service) + allow(UpdateRuntimeCompatibilityJob).to receive(:perform_later) end - it 'updates assignment compatibility before updating the runtime flow stream' do + it 'schedules runtime compatibility updates' do response = described_class.new(runtime, []).execute expect(response).to be_success - expect(Runtimes::CheckRuntimeCompatibilityService).to have_received(:new).with(runtime, project) - expect(assignment.reload.compatible).to be(true) - expect(FlowHandler).to have_received(:update_runtime).with(runtime) - end - - context 'when the runtime is not compatible with a project assignment' do - let(:compatibility_response) { ServiceResponse.error(message: 'Not compatible', error_code: :missing_definition) } - - it 'marks the assignment as incompatible before updating the runtime flow stream' do - assignment.update!(compatible: true) - - response = described_class.new(runtime, []).execute - - expect(response).to be_success - expect(assignment.reload.compatible).to be(false) - expect(FlowHandler).to have_received(:update_runtime).with(runtime) - end + expect(UpdateRuntimeCompatibilityJob).to have_received(:perform_later).with({ runtime_id: runtime.id }) end end end