diff --git a/Gemfile b/Gemfile index d99cd5970e4d9603a284a2c8c613a729cab26340..c8f3d5c685545d67003256bdc14f48278463950c 100644 --- a/Gemfile +++ b/Gemfile @@ -482,6 +482,9 @@ gem 'spamcheck', '~> 0.1.0' # Gitaly GRPC protocol definitions gem 'gitaly', '~> 13.12.0.pre.rc1' +# KAS GRPC protocol definitions +gem 'kas-grpc', '~> 0.0.2' + gem 'grpc', '~> 1.30.2' gem 'google-protobuf', '~> 3.17.1' diff --git a/Gemfile.lock b/Gemfile.lock index a1fd31680a16a4f19e125c9892686443777202f5..fc2576728222303ab4f5a7088d06de1abf819d6c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -682,6 +682,8 @@ GEM activerecord kaminari-core (= 1.2.1) kaminari-core (1.2.1) + kas-grpc (0.0.2) + grpc (~> 1.0) knapsack (1.21.1) rake kramdown (2.3.1) @@ -1520,6 +1522,7 @@ DEPENDENCIES json_schemer (~> 0.2.12) jwt (~> 2.1.0) kaminari (~> 1.0) + kas-grpc (~> 0.0.2) knapsack (~> 1.21.1) kramdown (~> 2.3.1) kubeclient (~> 4.9.1) diff --git a/lib/gitlab/kas.rb b/lib/gitlab/kas.rb index 7b2c792ebca7a58afcf2d8aa43a267afee00ceda..a4663314b3b8c2c52e1a17e04babd6f0c56e7ca8 100644 --- a/lib/gitlab/kas.rb +++ b/lib/gitlab/kas.rb @@ -45,6 +45,13 @@ def external_url Gitlab.config.gitlab_kas.external_url end + # Return GitLab KAS internal_url + # + # @return [String] internal_url + def internal_url + Gitlab.config.gitlab_kas.internal_url + end + # Return whether GitLab KAS is enabled # # @return [Boolean] external_url diff --git a/lib/gitlab/kas/client.rb b/lib/gitlab/kas/client.rb new file mode 100644 index 0000000000000000000000000000000000000000..6675903e6924197eb234a164b32c59a722983544 --- /dev/null +++ b/lib/gitlab/kas/client.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Gitlab + module Kas + class Client + TIMEOUT = 2.seconds.freeze + JWT_AUDIENCE = 'gitlab-kas' + + STUB_CLASSES = { + configuration_project: Gitlab::Agent::ConfigurationProject::Rpc::ConfigurationProject::Stub + }.freeze + + ConfigurationError = Class.new(StandardError) + + def initialize + raise ConfigurationError, 'GitLab KAS is not enabled' unless Gitlab::Kas.enabled? + raise ConfigurationError, 'KAS internal URL is not configured' unless Gitlab::Kas.internal_url.present? + end + + def list_agent_config_files(project:) + request = Gitlab::Agent::ConfigurationProject::Rpc::ListAgentConfigFilesRequest.new( + repository: repository(project), + gitaly_address: gitaly_address(project) + ) + + stub_for(:configuration_project) + .list_agent_config_files(request, metadata: metadata) + .config_files + .to_a + end + + private + + def stub_for(service) + @stubs ||= {} + @stubs[service] ||= STUB_CLASSES.fetch(service).new(kas_endpoint_url, credentials, timeout: TIMEOUT) + end + + def repository(project) + gitaly_repository = project.repository.gitaly_repository + + Gitlab::Agent::Modserver::Repository.new(gitaly_repository.to_h) + end + + def gitaly_address(project) + connection_data = Gitlab::GitalyClient.connection_data(project.repository_storage) + + Gitlab::Agent::Modserver::GitalyAddress.new(connection_data) + end + + def kas_endpoint_url + Gitlab::Kas.internal_url.delete_prefix('grpc://') + end + + def credentials + if Rails.env.test? || Rails.env.development? + :this_channel_is_insecure + else + GRPC::Core::ChannelCredentials.new + end + end + + def metadata + { 'authorization' => "bearer #{token}" } + end + + def token + JSONWebToken::HMACToken.new(Gitlab::Kas.secret).tap do |token| + token.issuer = Settings.gitlab.host + token.audience = JWT_AUDIENCE + end.encoded + end + end + end +end diff --git a/spec/lib/gitlab/kas/client_spec.rb b/spec/lib/gitlab/kas/client_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7bf2d30ca48b9747d1e171bed178b47d1a6f7d40 --- /dev/null +++ b/spec/lib/gitlab/kas/client_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Kas::Client do + let_it_be(:project) { create(:project) } + + describe '#initialize' do + context 'kas is not enabled' do + before do + allow(Gitlab::Kas).to receive(:enabled?).and_return(false) + end + + it 'raises a configuration error' do + expect { described_class.new }.to raise_error(described_class::ConfigurationError, 'GitLab KAS is not enabled') + end + end + + context 'internal url is not set' do + before do + allow(Gitlab::Kas).to receive(:enabled?).and_return(true) + allow(Gitlab::Kas).to receive(:internal_url).and_return(nil) + end + + it 'raises a configuration error' do + expect { described_class.new }.to raise_error(described_class::ConfigurationError, 'KAS internal URL is not configured') + end + end + end + + describe 'gRPC calls' do + let(:token) { instance_double(JSONWebToken::HMACToken, encoded: 'test-token') } + + before do + allow(Gitlab::Kas).to receive(:enabled?).and_return(true) + allow(Gitlab::Kas).to receive(:internal_url).and_return('grpc://example.kas.internal') + + expect(JSONWebToken::HMACToken).to receive(:new) + .with(Gitlab::Kas.secret) + .and_return(token) + + expect(token).to receive(:issuer=).with(Settings.gitlab.host) + expect(token).to receive(:audience=).with(described_class::JWT_AUDIENCE) + end + + describe '#list_agent_config_files' do + let(:stub) { instance_double(Gitlab::Agent::ConfigurationProject::Rpc::ConfigurationProject::Stub) } + + let(:request) { instance_double(Gitlab::Agent::ConfigurationProject::Rpc::ListAgentConfigFilesRequest) } + let(:response) { double(Gitlab::Agent::ConfigurationProject::Rpc::ListAgentConfigFilesResponse, config_files: agent_configurations) } + + let(:repository) { instance_double(Gitlab::Agent::Modserver::Repository) } + let(:gitaly_address) { instance_double(Gitlab::Agent::Modserver::GitalyAddress) } + + let(:agent_configurations) { [double] } + + subject { described_class.new.list_agent_config_files(project: project) } + + before do + expect(Gitlab::Agent::ConfigurationProject::Rpc::ConfigurationProject::Stub).to receive(:new) + .with('example.kas.internal', :this_channel_is_insecure, timeout: described_class::TIMEOUT) + .and_return(stub) + + expect(Gitlab::Agent::Modserver::Repository).to receive(:new) + .with(project.repository.gitaly_repository.to_h) + .and_return(repository) + + expect(Gitlab::Agent::Modserver::GitalyAddress).to receive(:new) + .with(Gitlab::GitalyClient.connection_data(project.repository_storage)) + .and_return(gitaly_address) + + expect(Gitlab::Agent::ConfigurationProject::Rpc::ListAgentConfigFilesRequest).to receive(:new) + .with(repository: repository, gitaly_address: gitaly_address) + .and_return(request) + + expect(stub).to receive(:list_agent_config_files) + .with(request, metadata: { 'authorization' => 'bearer test-token' }) + .and_return(response) + end + + it { expect(subject).to eq(agent_configurations) } + end + end +end diff --git a/spec/lib/gitlab/kas_spec.rb b/spec/lib/gitlab/kas_spec.rb index e323f76b42e8a75b8dc5e3f40ace88d5ff4cd907..c9d40f785b82da7ab6fce5e895cd3a38f46d904e 100644 --- a/spec/lib/gitlab/kas_spec.rb +++ b/spec/lib/gitlab/kas_spec.rb @@ -65,6 +65,12 @@ end end + describe '.internal_url' do + it 'returns gitlab_kas internal_url config' do + expect(described_class.internal_url).to eq(Gitlab.config.gitlab_kas.internal_url) + end + end + describe '.version' do it 'returns gitlab_kas version config' do version_file = Rails.root.join(described_class::VERSION_FILE)