Commit 6146a510 authored by Krasimir Angelov's avatar Krasimir Angelov Committed by Kamil Trzciński

Expose CI secrets configuration in API response

Update `/api/v4/jobs/request` endpoint to include CI secrets
configuration if job has any and the feature is available.

Inject Vault server configuration details to CI secrets API response
when runner is requesting a job. Server details are currently fetched
from CI variables, this will be moved to DB and UI later.

Related to https://gitlab.com/gitlab-org/gitlab/-/issues/28321
and https://gitlab.com/gitlab-org/gitlab/-/issues/218746.
parent 65b896d3
......@@ -118,3 +118,5 @@ module Ci
end
end
end
Ci::BuildRunnerPresenter.prepend_if_ee('EE::Ci::BuildRunnerPresenter')
......@@ -6,7 +6,7 @@ module EE
extend ActiveSupport::Concern
prepended do
delegate :secrets?, to: :metadata, prefix: false, allow_nil: true
delegate :secrets?, :secrets, to: :metadata, prefix: false, allow_nil: true
end
def secrets=(value)
......
......@@ -20,7 +20,7 @@ module EE
}.with_indifferent_access.freeze
EE_RUNNER_FEATURES = {
secrets: -> (build) { build.ci_secrets_management_available? && build.secrets?}
vault_secrets: -> (build) { build.ci_secrets_management_available? && build.secrets?}
}.freeze
prepended do
......
# frozen_string_literal: true
module EE
module Ci
module BuildRunnerPresenter
extend ActiveSupport::Concern
def secrets_configuration
secrets.to_h.transform_values do |secret|
secret['vault']['server'] = vault_server if secret['vault']
secret
end
end
private
def vault_server
@vault_server ||= {
'url' => variable_value('VAULT_SERVER_URL'),
'auth' => {
'name' => 'jwt',
'path' => 'jwt',
'data' => {
'jwt' => '${CI_JOB_JWT}',
'role' => variable_value('VAULT_AUTH_ROLE')
}.compact
}
}
end
def variable_value(key)
variables_hash[key]
end
def variables_hash
@variables_hash ||= variables.map do |variable|
[variable[:key], variable[:value]]
end.to_h
end
end
end
end
# frozen_string_literal: true
module EE
module API
module Entities
module JobRequest
module Response
extend ActiveSupport::Concern
prepended do
expose :secrets_configuration, as: :secrets, if: -> (build, _) { build.ci_secrets_management_available? }
end
end
end
end
end
end
......@@ -531,13 +531,13 @@ RSpec.describe Ci::Build do
context 'when there are secrets defined' do
let(:secrets) { valid_secrets }
it { is_expected.to include(:secrets) }
it { is_expected.to include(:vault_secrets) }
end
context 'when there are no secrets defined' do
let(:secrets) { {} }
it { is_expected.not_to include(:secrets) }
it { is_expected.not_to include(:vault_secrets) }
end
end
......@@ -549,13 +549,13 @@ RSpec.describe Ci::Build do
context 'when there are secrets defined' do
let(:secrets) { valid_secrets }
it { is_expected.not_to include(:secrets) }
it { is_expected.not_to include(:vault_secrets) }
end
context 'when there are no secrets defined' do
let(:secrets) { {} }
it { is_expected.not_to include(:secrets) }
it { is_expected.not_to include(:vault_secrets) }
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::BuildRunnerPresenter do
subject(:presenter) { described_class.new(ci_build) }
describe '#secrets_configuration' do
let!(:ci_build) { create(:ci_build, secrets: secrets) }
context 'build has no secrets' do
let(:secrets) { {} }
it 'returns empty hash' do
expect(presenter.secrets_configuration).to eq({})
end
end
context 'build has secrets' do
let(:secrets) do
{
DATABASE_PASSWORD: {
vault: {
engine: { name: 'kv-v2', path: 'kv-v2' },
path: 'production/db',
field: 'password'
}
}
}
end
context 'Vault server URL' do
let(:vault_server) { presenter.secrets_configuration.dig('DATABASE_PASSWORD', 'vault', 'server') }
context 'VAULT_SERVER_URL CI variable is present' do
it 'returns the URL' do
create(:ci_variable, project: ci_build.project, key: 'VAULT_SERVER_URL', value: 'https://vault.example.com')
expect(vault_server.fetch('url')).to eq('https://vault.example.com')
end
end
context 'VAULT_SERVER_URL CI variable is not present' do
it 'returns nil' do
expect(vault_server.fetch('url')).to be_nil
end
end
end
context 'Vault auth role' do
let(:vault_auth_data) { presenter.secrets_configuration.dig('DATABASE_PASSWORD', 'vault', 'server', 'auth', 'data') }
context 'VAULT_AUTH_ROLE CI variable is present' do
it 'contains the auth role' do
create(:ci_variable, project: ci_build.project, key: 'VAULT_AUTH_ROLE', value: 'production')
expect(vault_auth_data.fetch('role')).to eq('production')
end
end
context 'VAULT_AUTH_ROLE CI variable is not present' do
it 'skips the auth role' do
expect(vault_auth_data).not_to have_key('role')
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Ci::Runner do
let_it_be(:project) { create(:project, :repository) }
describe '/api/v4/jobs' do
let(:runner) { create(:ci_runner, :project, projects: [project]) }
describe 'POST /api/v4/jobs/request' do
context 'secrets management' do
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let(:valid_secrets) do
{
DATABASE_PASSWORD: {
vault: {
engine: { name: 'kv-v2', path: 'kv-v2' },
path: 'production/db',
field: 'password'
}
}
}
end
let!(:ci_build) { create(:ci_build, pipeline: pipeline, secrets: secrets) }
context 'when secrets management feature is available' do
before do
stub_licensed_features(ci_secrets_management: true)
end
context 'when job has secrets configured' do
let(:secrets) { valid_secrets }
context 'when runner does not support secrets' do
it 'sets "runner_unsupported" failure reason and does not expose the build at all' do
request_job
expect(ci_build.reload).to be_runner_unsupported
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'when runner supports secrets' do
before do
create(:ci_variable, project: project, key: 'VAULT_SERVER_URL', value: 'https://vault.example.com')
create(:ci_variable, project: project, key: 'VAULT_AUTH_ROLE', value: 'production')
end
it 'returns secrets configuration' do
request_job_with_secrets_supported
expect(response).to have_gitlab_http_status(:created)
expect(json_response['secrets']).to eq(
{
'DATABASE_PASSWORD' => {
'vault' => {
'server' => {
'url' => 'https://vault.example.com',
'auth' => {
'name' => 'jwt',
'path' => 'jwt',
'data' => {
'jwt' => '${CI_JOB_JWT}',
'role' => 'production'
}
}
},
'engine' => { 'name' => 'kv-v2', 'path' => 'kv-v2' },
'path' => 'production/db',
'field' => 'password'
}
}
}
)
end
end
end
context 'job does not have secrets configured' do
let(:secrets) { {} }
it 'doesn not return secrets configuration' do
request_job_with_secrets_supported
expect(response).to have_gitlab_http_status(:created)
expect(json_response['secrets']).to eq({})
end
end
end
context 'when secrets management feature is not available' do
before do
stub_licensed_features(ci_secrets_management: false)
end
context 'job has secrets configured' do
let(:secrets) { valid_secrets }
it 'doesn not return secrets configuration' do
request_job_with_secrets_supported
expect(response).to have_gitlab_http_status(:created)
expect(json_response['secrets']).to eq(nil)
end
end
end
end
def request_job_with_secrets_supported
request_job info: { features: { vault_secrets: true } }
end
end
def request_job(token = runner.token, **params)
post api('/jobs/request'), params: params.merge(token: token)
end
end
end
......@@ -33,3 +33,5 @@ module API
end
end
end
API::Entities::JobRequest::Response.prepend_if_ee('EE::API::Entities::JobRequest::Response')
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment