diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index 2a538fd037eedff41f3d3be0aa99902c3e309969..b771164c055fa602c329d388b2512332284c342d 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -21,76 +21,77 @@ See also: The following API resources are available in the project context: -| Resource | Available endpoints | -|:------------------------------------------------------------------------|:--------------------| -| [Access requests](access_requests.md) | `/projects/:id/access_requests` (also available for groups) | -| [Access tokens](resource_access_tokens.md) | `/projects/:id/access_tokens` (also available for groups) | -| [Award emoji](award_emoji.md) | `/projects/:id/issues/.../award_emoji`, `/projects/:id/merge_requests/.../award_emoji`, `/projects/:id/snippets/.../award_emoji` | -| [Branches](branches.md) | `/projects/:id/repository/branches/`, `/projects/:id/repository/merged_branches` | -| [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` | -| [Container Registry](container_registry.md) | `/projects/:id/registry/repositories` | -| [Custom attributes](custom_attributes.md) | `/projects/:id/custom_attributes` (also available for groups and users) | -| [Debian distributions](packages/debian_project_distributions.md) | `/projects/:id/debian_distributions` (also available for groups) | -| [Dependencies](dependencies.md) **(ULTIMATE)** | `/projects/:id/dependencies` | -| [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) | -| [Deploy tokens](deploy_tokens.md) | `/projects/:id/deploy_tokens` (also available for groups and standalone) | -| [Deployments](deployments.md) | `/projects/:id/deployments` | +| Resource | Available endpoints | +|:------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Access requests](access_requests.md) | `/projects/:id/access_requests` (also available for groups) | +| [Access tokens](resource_access_tokens.md) | `/projects/:id/access_tokens` (also available for groups) | +| [Agents](cluster_agents.md) | `/projects/:id/cluster_agents` | +| [Award emoji](award_emoji.md) | `/projects/:id/issues/.../award_emoji`, `/projects/:id/merge_requests/.../award_emoji`, `/projects/:id/snippets/.../award_emoji` | +| [Branches](branches.md) | `/projects/:id/repository/branches/`, `/projects/:id/repository/merged_branches` | +| [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` | +| [Container Registry](container_registry.md) | `/projects/:id/registry/repositories` | +| [Custom attributes](custom_attributes.md) | `/projects/:id/custom_attributes` (also available for groups and users) | +| [Debian distributions](packages/debian_project_distributions.md) | `/projects/:id/debian_distributions` (also available for groups) | +| [Dependencies](dependencies.md) **(ULTIMATE)** | `/projects/:id/dependencies` | +| [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) | +| [Deploy tokens](deploy_tokens.md) | `/projects/:id/deploy_tokens` (also available for groups and standalone) | +| [Deployments](deployments.md) | `/projects/:id/deployments` | | [Discussions](discussions.md) (threaded comments) | `/projects/:id/issues/.../discussions`, `/projects/:id/snippets/.../discussions`, `/projects/:id/merge_requests/.../discussions`, `/projects/:id/commits/.../discussions` (also available for groups) | -| [Environments](environments.md) | `/projects/:id/environments` | -| [Error Tracking](error_tracking.md) | `/projects/:id/error_tracking/settings` | -| [Events](events.md) | `/projects/:id/events` (also available for users and standalone) | -| [Feature Flag User Lists](feature_flag_user_lists.md) | `/projects/:id/feature_flags_user_lists` | -| [Feature Flags](feature_flags.md) | `/projects/:id/feature_flags` | -| [Freeze Periods](freeze_periods.md) | `/projects/:id/freeze_periods` | -| [Integrations](integrations.md) (Formerly "services") | `/projects/:id/integrations` | -| [Invitations](invitations.md) | `/projects/:id/invitations` (also available for groups) | -| [Issue boards](boards.md) | `/projects/:id/boards` | -| [Issue links](issue_links.md) | `/projects/:id/issues/.../links` | -| [Issues Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) | -| [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) | -| [Iterations](iterations.md) **(PREMIUM)** | `/projects/:id/iterations` (also available for groups) | -| [Jobs](jobs.md) | `/projects/:id/jobs`, `/projects/:id/pipelines/.../jobs` | -| [Labels](labels.md) | `/projects/:id/labels` | -| [Managed licenses](managed_licenses.md) **(ULTIMATE)** | `/projects/:id/managed_licenses` | -| [Members](members.md) | `/projects/:id/members` (also available for groups) | -| [Merge request approvals](merge_request_approvals.md) **(PREMIUM)** | `/projects/:id/approvals`, `/projects/:id/merge_requests/.../approvals` | -| [Merge requests](merge_requests.md) | `/projects/:id/merge_requests` (also available for groups and standalone) | -| [Merge trains](merge_trains.md) | `/projects/:id/merge_trains` | -| [Notes](notes.md) (comments) | `/projects/:id/issues/.../notes`, `/projects/:id/snippets/.../notes`, `/projects/:id/merge_requests/.../notes` (also available for groups) | -| [Notification settings](notification_settings.md) | `/projects/:id/notification_settings` (also available for groups and standalone) | -| [Packages](packages.md) | `/projects/:id/packages` | -| [Pages domains](pages_domains.md) | `/projects/:id/pages` (also available standalone) | -| [Pipeline schedules](pipeline_schedules.md) | `/projects/:id/pipeline_schedules` | -| [Pipeline triggers](pipeline_triggers.md) | `/projects/:id/triggers` | -| [Pipelines](pipelines.md) | `/projects/:id/pipelines` | -| [Project badges](project_badges.md) | `/projects/:id/badges` | -| [Project clusters](project_clusters.md) | `/projects/:id/clusters` | -| [Project import/export](project_import_export.md) | `/projects/:id/export`, `/projects/import`, `/projects/:id/import` | -| [Project milestones](milestones.md) | `/projects/:id/milestones` | -| [Project snippets](project_snippets.md) | `/projects/:id/snippets` | -| [Project templates](project_templates.md) | `/projects/:id/templates` | -| [Project vulnerabilities](project_vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/templates` | -| [Project wikis](wikis.md) | `/projects/:id/wikis` | -| [Project-level variables](project_level_variables.md) | `/projects/:id/variables` | -| [Projects](projects.md) including setting Webhooks | `/projects`, `/projects/:id/hooks` (also available for users) | -| [Protected branches](protected_branches.md) | `/projects/:id/protected_branches` | -| [Protected environments](protected_environments.md) | `/projects/:id/protected_environments` | -| [Protected tags](protected_tags.md) | `/projects/:id/protected_tags` | -| [Release links](releases/links.md) | `/projects/:id/releases/.../assets/links` | -| [Releases](releases/index.md) | `/projects/:id/releases` | -| [Remote mirrors](remote_mirrors.md) | `/projects/:id/remote_mirrors` | -| [Repositories](repositories.md) | `/projects/:id/repository` | -| [Repository files](repository_files.md) | `/projects/:id/repository/files` | -| [Repository submodules](repository_submodules.md) | `/projects/:id/repository/submodules` | -| [Resource label events](resource_label_events.md) | `/projects/:id/issues/.../resource_label_events`, `/projects/:id/merge_requests/.../resource_label_events` (also available for groups) | -| [Runners](runners.md) | `/projects/:id/runners` (also available standalone) | -| [Search](search.md) | `/projects/:id/search` (also available for groups and standalone) | -| [Tags](tags.md) | `/projects/:id/repository/tags` | -| [User-starred metrics dashboards](metrics_user_starred_dashboards.md ) | `/projects/:id/metrics/user_starred_dashboards` | -| [Visual Review discussions](visual_review_discussions.md) **(PREMIUM)** | `/projects/:id/merge_requests/:merge_request_id/visual_review_discussions` | -| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/vulnerabilities/:id` | -| [Vulnerability exports](vulnerability_exports.md) **(ULTIMATE)** | `/projects/:id/vulnerability_exports` | -| [Vulnerability findings](vulnerability_findings.md) **(ULTIMATE)** | `/projects/:id/vulnerability_findings` | +| [Environments](environments.md) | `/projects/:id/environments` | +| [Error Tracking](error_tracking.md) | `/projects/:id/error_tracking/settings` | +| [Events](events.md) | `/projects/:id/events` (also available for users and standalone) | +| [Feature Flag User Lists](feature_flag_user_lists.md) | `/projects/:id/feature_flags_user_lists` | +| [Feature Flags](feature_flags.md) | `/projects/:id/feature_flags` | +| [Freeze Periods](freeze_periods.md) | `/projects/:id/freeze_periods` | +| [Integrations](integrations.md) (Formerly "services") | `/projects/:id/integrations` | +| [Invitations](invitations.md) | `/projects/:id/invitations` (also available for groups) | +| [Issue boards](boards.md) | `/projects/:id/boards` | +| [Issue links](issue_links.md) | `/projects/:id/issues/.../links` | +| [Issues Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) | +| [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) | +| [Iterations](iterations.md) **(PREMIUM)** | `/projects/:id/iterations` (also available for groups) | +| [Jobs](jobs.md) | `/projects/:id/jobs`, `/projects/:id/pipelines/.../jobs` | +| [Labels](labels.md) | `/projects/:id/labels` | +| [Managed licenses](managed_licenses.md) **(ULTIMATE)** | `/projects/:id/managed_licenses` | +| [Members](members.md) | `/projects/:id/members` (also available for groups) | +| [Merge request approvals](merge_request_approvals.md) **(PREMIUM)** | `/projects/:id/approvals`, `/projects/:id/merge_requests/.../approvals` | +| [Merge requests](merge_requests.md) | `/projects/:id/merge_requests` (also available for groups and standalone) | +| [Merge trains](merge_trains.md) | `/projects/:id/merge_trains` | +| [Notes](notes.md) (comments) | `/projects/:id/issues/.../notes`, `/projects/:id/snippets/.../notes`, `/projects/:id/merge_requests/.../notes` (also available for groups) | +| [Notification settings](notification_settings.md) | `/projects/:id/notification_settings` (also available for groups and standalone) | +| [Packages](packages.md) | `/projects/:id/packages` | +| [Pages domains](pages_domains.md) | `/projects/:id/pages` (also available standalone) | +| [Pipeline schedules](pipeline_schedules.md) | `/projects/:id/pipeline_schedules` | +| [Pipeline triggers](pipeline_triggers.md) | `/projects/:id/triggers` | +| [Pipelines](pipelines.md) | `/projects/:id/pipelines` | +| [Project badges](project_badges.md) | `/projects/:id/badges` | +| [Project clusters](project_clusters.md) | `/projects/:id/clusters` | +| [Project import/export](project_import_export.md) | `/projects/:id/export`, `/projects/import`, `/projects/:id/import` | +| [Project milestones](milestones.md) | `/projects/:id/milestones` | +| [Project snippets](project_snippets.md) | `/projects/:id/snippets` | +| [Project templates](project_templates.md) | `/projects/:id/templates` | +| [Project vulnerabilities](project_vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/templates` | +| [Project wikis](wikis.md) | `/projects/:id/wikis` | +| [Project-level variables](project_level_variables.md) | `/projects/:id/variables` | +| [Projects](projects.md) including setting Webhooks | `/projects`, `/projects/:id/hooks` (also available for users) | +| [Protected branches](protected_branches.md) | `/projects/:id/protected_branches` | +| [Protected environments](protected_environments.md) | `/projects/:id/protected_environments` | +| [Protected tags](protected_tags.md) | `/projects/:id/protected_tags` | +| [Release links](releases/links.md) | `/projects/:id/releases/.../assets/links` | +| [Releases](releases/index.md) | `/projects/:id/releases` | +| [Remote mirrors](remote_mirrors.md) | `/projects/:id/remote_mirrors` | +| [Repositories](repositories.md) | `/projects/:id/repository` | +| [Repository files](repository_files.md) | `/projects/:id/repository/files` | +| [Repository submodules](repository_submodules.md) | `/projects/:id/repository/submodules` | +| [Resource label events](resource_label_events.md) | `/projects/:id/issues/.../resource_label_events`, `/projects/:id/merge_requests/.../resource_label_events` (also available for groups) | +| [Runners](runners.md) | `/projects/:id/runners` (also available standalone) | +| [Search](search.md) | `/projects/:id/search` (also available for groups and standalone) | +| [Tags](tags.md) | `/projects/:id/repository/tags` | +| [User-starred metrics dashboards](metrics_user_starred_dashboards.md ) | `/projects/:id/metrics/user_starred_dashboards` | +| [Visual Review discussions](visual_review_discussions.md) **(PREMIUM)** | `/projects/:id/merge_requests/:merge_request_id/visual_review_discussions` | +| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/vulnerabilities/:id` | +| [Vulnerability exports](vulnerability_exports.md) **(ULTIMATE)** | `/projects/:id/vulnerability_exports` | +| [Vulnerability findings](vulnerability_findings.md) **(ULTIMATE)** | `/projects/:id/vulnerability_findings` | ## Group resources diff --git a/doc/api/cluster_agents.md b/doc/api/cluster_agents.md new file mode 100644 index 0000000000000000000000000000000000000000..37cc4a243426f27b629027a5f8ce34aecd1b20a4 --- /dev/null +++ b/doc/api/cluster_agents.md @@ -0,0 +1,238 @@ +--- +stage: Configure +group: Configure +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Agents API **(FREE)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83270) in GitLab 14.10. + +Use the Agents API to work with the GitLab agent for Kubernetes. + +## List the agents for a project + +Returns the list of agents registered for the project. + +You must have at least the Developer role to use this endpoint. + +```plaintext +GET /projects/:id/cluster_agents +``` + +Parameters: + +| Attribute | Type | Required | Description | +|-----------|-------------------|-----------|-----------------------------------------------------------------------------------------------------------------| +| `id` | integer or string | yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) maintained by the authenticated user | + +Response: + +The response is a list of agents with the following fields: + +| Attribute | Type | Description | +|--------------------------------------|----------|------------------------------------------------------| +| `id` | integer | ID of the agent | +| `name` | string | Name of the agent | +| `config_project` | object | Object representing the project the agent belongs to | +| `config_project.id` | integer | ID of the project | +| `config_project.description` | string | Description of the project | +| `config_project.name` | string | Name of the project | +| `config_project.name_with_namespace` | string | Full name with namespace of the project | +| `config_project.path` | string | Path to the project | +| `config_project.path_with_namespace` | string | Full path with namespace to the project | +| `config_project.created_at` | string | ISO8601 datetime when the project was created | +| `created_at` | string | ISO8601 datetime when the agent was created | +| `created_by_user_id` | integer | ID of the user who created the agent | + +Example request: + +```shell +curl --header "Private-Token: " "https://gitlab.example.com/api/v4/projects/20/cluster_agents" +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "agent-1", + "config_project": { + "id": 20, + "description": "", + "name": "test", + "name_with_namespace": "Administrator / test", + "path": "test", + "path_with_namespace": "root/test", + "created_at": "2022-03-20T20:42:40.221Z" + }, + "created_at": "2022-04-20T20:42:40.221Z", + "created_by_user_id": 42 + }, + { + "id": 2, + "name": "agent-2", + "config_project": { + "id": 20, + "description": "", + "name": "test", + "name_with_namespace": "Administrator / test", + "path": "test", + "path_with_namespace": "root/test", + "created_at": "2022-03-20T20:42:40.221Z" + }, + "created_at": "2022-04-20T20:42:40.221Z", + "created_by_user_id": 42 + } +] +``` + +## Get details about an agent + +Gets a single agent details. + +You must have at least the Developer role to use this endpoint. + +```shell +GET /projects/:id/cluster_agents/:agent_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +|------------|-------------------|----------|-----------------------------------------------------------------------------------------------------------------| +| `id` | integer or string | yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) maintained by the authenticated user | +| `agent_id` | integer | yes | ID of the agent | + +Response: + +The response is a single agent with the following fields: + +| Attribute | Type | Description | +|--------------------------------------|---------|------------------------------------------------------| +| `id` | integer | ID of the agent | +| `name` | string | Name of the agent | +| `config_project` | object | Object representing the project the agent belongs to | +| `config_project.id` | integer | ID of the project | +| `config_project.description` | string | Description of the project | +| `config_project.name` | string | Name of the project | +| `config_project.name_with_namespace` | string | Full name with namespace of the project | +| `config_project.path` | string | Path to the project | +| `config_project.path_with_namespace` | string | Full path with namespace to the project | +| `config_project.created_at` | string | ISO8601 datetime when the project was created | +| `created_at` | string | ISO8601 datetime when the agent was created | +| `created_by_user_id` | integer | ID of the user who created the agent | + +Example request: + +```shell +curl --header "Private-Token: " "https://gitlab.example.com/api/v4/projects/20/cluster_agents/1" +``` + +Example response: + +```json +{ + "id": 1, + "name": "agent-1", + "config_project": { + "id": 20, + "description": "", + "name": "test", + "name_with_namespace": "Administrator / test", + "path": "test", + "path_with_namespace": "root/test", + "created_at": "2022-03-20T20:42:40.221Z" + }, + "created_at": "2022-04-20T20:42:40.221Z", + "created_by_user_id": 42 +} +``` + +## Register an agent with a project + +Registers an agent to the project. + +You must have at least the Maintainer role to use this endpoint. + +```shell +POST /projects/:id/cluster_agents +``` + +Parameters: + +| Attribute | Type | Required | Description | +|-----------|-------------------|----------|-----------------------------------------------------------------------------------------------------------------| +| `id` | integer or string | yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) maintained by the authenticated user | +| `name` | string | yes | Name for the agent | + +Response: + +The response is the new agent with the following fields: + +| Attribute | Type | Description | +|--------------------------------------|---------|------------------------------------------------------| +| `id` | integer | ID of the agent | +| `name` | string | Name of the agent | +| `config_project` | object | Object representing the project the agent belongs to | +| `config_project.id` | integer | ID of the project | +| `config_project.description` | string | Description of the project | +| `config_project.name` | string | Name of the project | +| `config_project.name_with_namespace` | string | Full name with namespace of the project | +| `config_project.path` | string | Path to the project | +| `config_project.path_with_namespace` | string | Full path with namespace to the project | +| `config_project.created_at` | string | ISO8601 datetime when the project was created | +| `created_at` | string | ISO8601 datetime when the agent was created | +| `created_by_user_id` | integer | ID of the user who created the agent | + +Example request: + +```shell +curl --header "Private-Token: " "https://gitlab.example.com/api/v4/projects/20/cluster_agents" \ + -H "Content-Type:application/json" \ + -X POST --data '{"name":"some-agent"}' +``` + +Example response: + +```json +{ + "id": 1, + "name": "agent-1", + "config_project": { + "id": 20, + "description": "", + "name": "test", + "name_with_namespace": "Administrator / test", + "path": "test", + "path_with_namespace": "root/test", + "created_at": "2022-03-20T20:42:40.221Z" + }, + "created_at": "2022-04-20T20:42:40.221Z", + "created_by_user_id": 42 +} +``` + +## Delete a registered agent + +Deletes an existing agent registration. + +You must have at least the Maintainer role to use this endpoint. + +```plaintext +DELETE /projects/:id/cluster_agents/:agent_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +|------------|-------------------|----------|-----------------------------------------------------------------------------------------------------------------| +| `id` | integer or string | yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) maintained by the authenticated user | +| `agent_id` | integer | yes | ID of the agent | + +Example request: + +```shell +curl --request DELETE --header "Private-Token: " "https://gitlab.example.com/api/v4/projects/20/cluster_agents/1 +``` diff --git a/lib/api/admin/instance_clusters.rb b/lib/api/admin/instance_clusters.rb index 4aebd9c0d40b565542c6846ec90b9cf117b18b14..d6c212a98864055fe0a31ebb75fe7a0c6f88d877 100644 --- a/lib/api/admin/instance_clusters.rb +++ b/lib/api/admin/instance_clusters.rb @@ -112,7 +112,7 @@ class InstanceClusters < ::API::Base helpers do def clusterable_instance - Clusters::Instance.new + ::Clusters::Instance.new end def clusters_for_current_user diff --git a/lib/api/api.rb b/lib/api/api.rb index 5100ec9ec9dbeeb23567ca49af6b848c0f8bb181..c273d5b4fb28c7393b170a505509dadde5e4c4e8 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -178,6 +178,7 @@ class API < ::API::Base 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 diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb index 794d2bbe3b212309e7f7787c10502b592b3471c9..86897eb61ae6ab20c4c59d0b4047d93e62e8e249 100644 --- a/lib/api/ci/jobs.rb +++ b/lib/api/ci/jobs.rb @@ -197,7 +197,7 @@ class Jobs < ::API::Base 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 diff --git a/lib/api/clusters/agents.rb b/lib/api/clusters/agents.rb new file mode 100644 index 0000000000000000000000000000000000000000..6c1bf21b952fe00423863afa7e48a987fe183766 --- /dev/null +++ b/lib/api/clusters/agents.rb @@ -0,0 +1,81 @@ +# 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 diff --git a/lib/api/entities/clusters/agent.rb b/lib/api/entities/clusters/agent.rb index 3b4538b81c256c8a283477bd968433aa89c14869..140b680f5e872e734bc49d586940fa95c088f9fe 100644 --- a/lib/api/entities/clusters/agent.rb +++ b/lib/api/entities/clusters/agent.rb @@ -5,7 +5,10 @@ module Entities 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 diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb index df887a83c4f91176e5840972be37e164afb535b5..59bc917a6029f84c589b4368f456bac5b84337bc 100644 --- a/lib/api/internal/kubernetes.rb +++ b/lib/api/internal/kubernetes.rb @@ -54,7 +54,7 @@ def check_feature_enabled 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 @@ def check_agent_token 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 diff --git a/lib/api/metrics/dashboard/annotations.rb b/lib/api/metrics/dashboard/annotations.rb index 0989340b3ea13e6b188e5ec6a5db83299daa5744..c6406bf61dff5a6bbefe28f868959f912c285e54 100644 --- a/lib/api/metrics/dashboard/annotations.rb +++ b/lib/api/metrics/dashboard/annotations.rb @@ -12,7 +12,7 @@ class Annotations < ::API::Base 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| diff --git a/spec/fixtures/api/schemas/public_api/v4/agent.json b/spec/fixtures/api/schemas/public_api/v4/agent.json new file mode 100644 index 0000000000000000000000000000000000000000..4821d5e0b047a86d9e9bcd5b127603857a8fbc3a --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/agent.json @@ -0,0 +1,18 @@ +{ + "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 +} diff --git a/spec/fixtures/api/schemas/public_api/v4/agents.json b/spec/fixtures/api/schemas/public_api/v4/agents.json new file mode 100644 index 0000000000000000000000000000000000000000..5fe3d7f94816f2e0e2c5fc9c225619c15d89299b --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/agents.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "agent.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/project_identity.json b/spec/fixtures/api/schemas/public_api/v4/project_identity.json new file mode 100644 index 0000000000000000000000000000000000000000..6471dd560c57d6dfd8b01b7093ff3dc169b0c6e5 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/project_identity.json @@ -0,0 +1,22 @@ +{ + "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 +} diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb index f279e779de5a1bcb87a4a824e88c256f3cf1460f..f10e0cc8fa7165ba1b7a3c185479567c1081a24e 100644 --- a/spec/models/clusters/agent_spec.rb +++ b/spec/models/clusters/agent_spec.rb @@ -117,6 +117,23 @@ 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) } diff --git a/spec/requests/api/clusters/agents_spec.rb b/spec/requests/api/clusters/agents_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e29be255289a5e92b65ccfaedfcd706867812186 --- /dev/null +++ b/spec/requests/api/clusters/agents_spec.rb @@ -0,0 +1,153 @@ +# 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