From 1377a970641d40ac9915ac6605edd47b2cadf044 Mon Sep 17 00:00:00 2001
From: Tymm Schmitke <tschmitke@gitlab.com>
Date: Wed, 15 Feb 2023 16:00:38 +0000
Subject: [PATCH] Add CreateRunnerService

---
 .../ci/runners/create_runner_service.rb       |  43 ++++++
 .../instance_runner_strategy.rb               |  29 ++++
 .../ci/runners/create_runner_service_spec.rb  | 135 ++++++++++++++++++
 3 files changed, 207 insertions(+)
 create mode 100644 app/services/ci/runners/create_runner_service.rb
 create mode 100644 app/services/ci/runners/runner_creation_strategies/instance_runner_strategy.rb
 create mode 100644 spec/services/ci/runners/create_runner_service_spec.rb

diff --git a/app/services/ci/runners/create_runner_service.rb b/app/services/ci/runners/create_runner_service.rb
new file mode 100644
index 0000000000000..2de9ee4d38ee9
--- /dev/null
+++ b/app/services/ci/runners/create_runner_service.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Ci
+  module Runners
+    class CreateRunnerService
+      RUNNER_CLASS_MAPPING = {
+        'instance_type' => Ci::Runners::RunnerCreationStrategies::InstanceRunnerStrategy,
+        nil => Ci::Runners::RunnerCreationStrategies::InstanceRunnerStrategy
+      }.freeze
+
+      attr_accessor :user, :type, :params, :strategy
+
+      def initialize(user:, type:, params:)
+        @user = user
+        @type = type
+        @params = params
+        @strategy = RUNNER_CLASS_MAPPING[type].new(user: user, type: type, params: params)
+      end
+
+      def execute
+        normalize_params
+
+        return ServiceResponse.error(message: 'Validation error') unless strategy.validate_params
+        return ServiceResponse.error(message: 'Insufficient permissions') unless strategy.authorized_user?
+
+        runner = ::Ci::Runner.new(params)
+
+        return ServiceResponse.success(payload: { runner: runner }) if runner.save
+
+        ServiceResponse.error(message: runner.errors.full_messages)
+      end
+
+      def normalize_params
+        params[:registration_type] = :authenticated_user
+        params[:runner_type] = type
+        params[:active] = !params.delete(:paused) if params[:paused].present?
+        params[:creator] = user
+
+        strategy.normalize_params
+      end
+    end
+  end
+end
diff --git a/app/services/ci/runners/runner_creation_strategies/instance_runner_strategy.rb b/app/services/ci/runners/runner_creation_strategies/instance_runner_strategy.rb
new file mode 100644
index 0000000000000..f195c3e88f9bc
--- /dev/null
+++ b/app/services/ci/runners/runner_creation_strategies/instance_runner_strategy.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Ci
+  module Runners
+    module RunnerCreationStrategies
+      class InstanceRunnerStrategy
+        attr_accessor :user, :type, :params
+
+        def initialize(user:, type:, params:)
+          @user = user
+          @type = type
+          @params = params
+        end
+
+        def normalize_params
+          params[:runner_type] = :instance_type
+        end
+
+        def validate_params
+          true
+        end
+
+        def authorized_user?
+          user.present? && user.can?(:create_instance_runners)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/services/ci/runners/create_runner_service_spec.rb b/spec/services/ci/runners/create_runner_service_spec.rb
new file mode 100644
index 0000000000000..673bf3ef90e8b
--- /dev/null
+++ b/spec/services/ci/runners/create_runner_service_spec.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::Runners::CreateRunnerService, "#execute", feature_category: :runner_fleet do
+  subject(:execute) { described_class.new(user: current_user, type: type, params: params).execute }
+
+  let(:runner) { execute.payload[:runner] }
+
+  let_it_be(:admin) { create(:admin) }
+  let_it_be(:non_admin_user) { create(:user) }
+  let_it_be(:anonymous) { nil }
+
+  shared_context 'when admin user' do
+    let(:current_user) { admin }
+
+    before do
+      allow(current_user).to receive(:can?).with(:create_instance_runners).and_return true
+    end
+  end
+
+  shared_examples 'it can create a runner' do
+    it 'creates a runner of the specified type' do
+      expect(runner.runner_type).to eq expected_type
+    end
+
+    context 'with default params provided' do
+      let(:args) do
+        {}
+      end
+
+      before do
+        params.merge!(args)
+      end
+
+      it { is_expected.to be_success }
+
+      it 'uses default values when none are provided' do
+        expect(runner).to be_an_instance_of(::Ci::Runner)
+        expect(runner.persisted?).to be_truthy
+        expect(runner.run_untagged).to be true
+        expect(runner.active).to be true
+        expect(runner.creator).to be current_user
+        expect(runner.authenticated_user_registration_type?).to be_truthy
+        expect(runner.runner_type).to eq 'instance_type'
+      end
+    end
+
+    context 'with non-default params provided' do
+      let(:args) do
+        {
+          description: 'some description',
+          maintenance_note: 'a note',
+          paused: true,
+          tag_list: %w[tag1 tag2],
+          access_level: 'ref_protected',
+          locked: true,
+          maximum_timeout: 600,
+          run_untagged: false
+        }
+      end
+
+      before do
+        params.merge!(args)
+      end
+
+      it { is_expected.to be_success }
+
+      it 'creates runner with specified values', :aggregate_failures do
+        expect(runner).to be_an_instance_of(::Ci::Runner)
+        expect(runner.description).to eq 'some description'
+        expect(runner.maintenance_note).to eq 'a note'
+        expect(runner.active).to eq !args[:paused]
+        expect(runner.locked).to eq args[:locked]
+        expect(runner.run_untagged).to eq args[:run_untagged]
+        expect(runner.tags).to contain_exactly(
+          an_object_having_attributes(name: 'tag1'),
+          an_object_having_attributes(name: 'tag2')
+        )
+        expect(runner.access_level).to eq args[:access_level]
+        expect(runner.maximum_timeout).to eq args[:maximum_timeout]
+
+        expect(runner.authenticated_user_registration_type?).to be_truthy
+        expect(runner.runner_type).to eq 'instance_type'
+      end
+    end
+  end
+
+  shared_examples 'it cannot create a runner' do
+    it 'runner payload is nil' do
+      expect(runner).to be nil
+    end
+
+    it { is_expected.to be_error }
+  end
+
+  shared_examples 'it can return an error' do
+    let(:group) { create(:group) }
+    let(:runner_double) { Ci::Runner.new }
+
+    context 'when the runner fails to save' do
+      before do
+        allow(Ci::Runner).to receive(:new).and_return runner_double
+      end
+
+      it_behaves_like 'it cannot create a runner'
+
+      it 'returns error message' do
+        expect(execute.errors).not_to be_empty
+      end
+    end
+  end
+
+  context 'with type param set to nil' do
+    let(:expected_type) { 'instance_type' }
+    let(:type) { nil }
+    let(:params) { {} }
+
+    it_behaves_like 'it cannot create a runner' do
+      let(:current_user) { anonymous }
+    end
+
+    it_behaves_like 'it cannot create a runner' do
+      let(:current_user) { non_admin_user }
+    end
+
+    it_behaves_like 'it can create a runner' do
+      include_context 'when admin user'
+    end
+
+    it_behaves_like 'it can return an error' do
+      include_context 'when admin user'
+    end
+  end
+end
-- 
GitLab