Commit 558a58d9 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'feature/agents-rest-api' into 'master'

Implement first iteration of the agents REST API

See merge request gitlab-org/gitlab!83270
parents e0a88539 f7b265a5
This diff is collapsed.
This diff is collapsed.
......@@ -112,7 +112,7 @@ module API
helpers do
def clusterable_instance
Clusters::Instance.new
::Clusters::Instance.new
end
def clusters_for_current_user
......
......@@ -182,6 +182,7 @@ module API
mount ::API::Ci::SecureFiles
mount ::API::Ci::Triggers
mount ::API::Ci::Variables
mount ::API::Clusters::Agents
mount ::API::Commits
mount ::API::CommitStatuses
mount ::API::ContainerRegistryEvent
......
......@@ -197,7 +197,7 @@ module API
pipeline = current_authenticated_job.pipeline
project = current_authenticated_job.project
agent_authorizations = Clusters::AgentAuthorizationsFinder.new(project).execute
agent_authorizations = ::Clusters::AgentAuthorizationsFinder.new(project).execute
project_groups = project.group&.self_and_ancestor_ids&.map { |id| { id: id } } || []
user_access_level = project.team.max_member_access(current_user.id)
roles_in_project = Gitlab::Access.sym_options_with_owner
......
# frozen_string_literal: true
module API
module Clusters
class Agents < ::API::Base
include PaginationParams
before { authenticate! }
feature_category :kubernetes_management
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'List agents' do
detail 'This feature was introduced in GitLab 14.10.'
success Entities::Clusters::Agent
end
params do
use :pagination
end
get ':id/cluster_agents' do
authorize! :read_cluster, user_project
agents = ::Clusters::AgentsFinder.new(user_project, current_user).execute
present paginate(agents), with: Entities::Clusters::Agent
end
desc 'Get single agent' do
detail 'This feature was introduced in GitLab 14.10.'
success Entities::Clusters::Agent
end
params do
requires :agent_id, type: Integer, desc: 'The ID of an agent'
end
get ':id/cluster_agents/:agent_id' do
authorize! :read_cluster, user_project
agent = user_project.cluster_agents.find(params[:agent_id])
present agent, with: Entities::Clusters::Agent
end
desc 'Add an agent to a project' do
detail 'This feature was introduced in GitLab 14.10.'
success Entities::Clusters::Agent
end
params do
requires :name, type: String, desc: 'The name of the agent'
end
post ':id/cluster_agents' do
authorize! :create_cluster, user_project
params = declared_params(include_missing: false)
result = ::Clusters::Agents::CreateService.new(user_project, current_user).execute(name: params[:name])
bad_request!(result[:message]) if result[:status] == :error
present result[:cluster_agent], with: Entities::Clusters::Agent
end
desc 'Delete an agent' do
detail 'This feature was introduced in GitLab 14.10.'
end
params do
requires :agent_id, type: Integer, desc: 'The ID of an agent'
end
delete ':id/cluster_agents/:agent_id' do
authorize! :admin_cluster, user_project
agent = user_project.cluster_agents.find(params.delete(:agent_id))
destroy_conditionally!(agent)
end
end
end
end
end
......@@ -5,7 +5,10 @@ module API
module Clusters
class Agent < Grape::Entity
expose :id
expose :name
expose :project, with: Entities::ProjectIdentity, as: :config_project
expose :created_at
expose :created_by_user_id
end
end
end
......
......@@ -54,7 +54,7 @@ module API
def check_agent_token
unauthorized! unless agent_token
Clusters::AgentTokens::TrackUsageService.new(agent_token).execute
::Clusters::AgentTokens::TrackUsageService.new(agent_token).execute
end
end
......@@ -91,9 +91,9 @@ module API
requires :agent_config, type: JSON, desc: 'Configuration for the Agent'
end
post '/' do
agent = Clusters::Agent.find(params[:agent_id])
agent = ::Clusters::Agent.find(params[:agent_id])
Clusters::Agents::RefreshAuthorizationService.new(agent, config: params[:agent_config]).execute
::Clusters::Agents::RefreshAuthorizationService.new(agent, config: params[:agent_config]).execute
no_content!
end
......
......@@ -12,7 +12,7 @@ module API
ANNOTATIONS_SOURCES = [
{ class: ::Environment, resource: :environments, create_service_param_key: :environment },
{ class: Clusters::Cluster, resource: :clusters, create_service_param_key: :cluster }
{ class: ::Clusters::Cluster, resource: :clusters, create_service_param_key: :cluster }
].freeze
ANNOTATIONS_SOURCES.each do |annotations_source|
......
{
"type": "object",
"required": [
"id",
"name",
"config_project",
"created_at",
"created_by_user_id"
],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"config_project": { "$ref": "project_identity.json" },
"created_at": { "type": "string", "format": "date-time" },
"created_by_user_id": { "type": "integer" }
},
"additionalProperties": false
}
{
"type": "array",
"items": { "$ref": "agent.json" }
}
{
"type": "object",
"required": [
"id",
"description",
"name",
"name_with_namespace",
"path",
"path_with_namespace",
"created_at"
],
"properties": {
"id": { "type": "integer" },
"description": { "type": ["string", "null"] },
"name": { "type": "string" },
"name_with_namespace": { "type": "string" },
"path": { "type": "string" },
"path_with_namespace": { "type": "string" },
"created_at": { "type": "string", "format": "date-time" }
},
"additionalProperties": false
}
......@@ -117,6 +117,23 @@ RSpec.describe Clusters::Agent do
end
end
describe '#last_used_agent_tokens' do
let_it_be(:agent) { create(:cluster_agent) }
subject { agent.last_used_agent_tokens }
context 'agent has no tokens' do
it { is_expected.to be_empty }
end
context 'agent has active and inactive tokens' do
let!(:active_token) { create(:cluster_agent_token, agent: agent, last_used_at: 1.minute.ago) }
let!(:inactive_token) { create(:cluster_agent_token, agent: agent, last_used_at: 2.hours.ago) }
it { is_expected.to contain_exactly(active_token, inactive_token) }
end
end
describe '#activity_event_deletion_cutoff' do
let_it_be(:agent) { create(:cluster_agent) }
let_it_be(:event1) { create(:agent_activity_event, agent: agent, recorded_at: 1.hour.ago) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Clusters::Agents do
let_it_be(:agent) { create(:cluster_agent) }
let(:user) { agent.created_by_user }
let(:unauthorized_user) { create(:user) }
let!(:project) { agent.project }
before do
project.add_maintainer(user)
end
describe 'GET /projects/:id/cluster_agents' do
context 'authorized user' do
it 'returns project agents' do
get api("/projects/#{project.id}/cluster_agents", user)
aggregate_failures "testing response" do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/agents')
expect(json_response.count).to eq(1)
expect(json_response.first['name']).to eq(agent.name)
end
end
end
context 'unauthorized user' do
it 'unable to access agents' do
get api("/projects/#{project.id}/cluster_agents", unauthorized_user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
it 'avoids N+1 queries', :request_store do
# Establish baseline
get api("/projects/#{project.id}/cluster_agents", user)
control = ActiveRecord::QueryRecorder.new do
get api("/projects/#{project.id}/cluster_agents", user)
end
# Now create a second record and ensure that the API does not execute
# any more queries than before
create(:cluster_agent, project: project)
expect do
get api("/projects/#{project.id}/cluster_agents", user)
end.not_to exceed_query_limit(control)
end
end
describe 'GET /projects/:id/cluster_agents/:agent_id' do
context 'authorized user' do
it 'returns a project agent' do
get api("/projects/#{project.id}/cluster_agents/#{agent.id}", user)
aggregate_failures "testing response" do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/agent')
expect(json_response['name']).to eq(agent.name)
end
end
it 'returns a 404 error if agent id is not available' do
get api("/projects/#{project.id}/cluster_agents/#{non_existing_record_id}", user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'unauthorized user' do
it 'unable to access an existing agent' do
get api("/projects/#{project.id}/cluster_agents/#{agent.id}", unauthorized_user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'POST /projects/:id/cluster_agents' do
it 'adds agent to project' do
expect do
post(api("/projects/#{project.id}/cluster_agents", user),
params: { name: 'some-agent' })
end.to change {project.cluster_agents.count}.by(1)
aggregate_failures "testing response" do
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/agent')
expect(json_response['name']).to eq('some-agent')
end
end
it 'returns a 400 error if name not given' do
post api("/projects/#{project.id}/cluster_agents", user)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns a 400 error if name is invalid' do
post api("/projects/#{project.id}/cluster_agents", user), params: { name: '#4^x' }
aggregate_failures "testing response" do
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message'])
.to include("Name can contain only lowercase letters, digits, and '-', but cannot start or end with '-'")
end
end
it 'returns 404 error if project does not exist' do
post api("/projects/#{non_existing_record_id}/cluster_agents", user), params: { name: 'some-agent' }
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'DELETE /projects/:id/cluster_agents/:agent_id' do
it 'deletes agent from project' do
expect do
delete api("/projects/#{project.id}/cluster_agents/#{agent.id}", user)
expect(response).to have_gitlab_http_status(:no_content)
end.to change {project.cluster_agents.count}.by(-1)
end
it 'returns a 404 error when deleting non existent agent' do
delete api("/projects/#{project.id}/cluster_agents/#{non_existing_record_id}", user)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns a 404 error if agent id not given' do
delete api("/projects/#{project.id}/cluster_agents", user)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns a 404 if the user is unauthorized to delete' do
delete api("/projects/#{project.id}/cluster_agents/#{agent.id}", unauthorized_user)
expect(response).to have_gitlab_http_status(:not_found)
end
it_behaves_like '412 response' do
let(:request) { api("/projects/#{project.id}/cluster_agents/#{agent.id}", user) }
end
end
end
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