Commit ed8bd983 authored by Mayra Cabrera's avatar Mayra Cabrera

Include CRUD endpoints for Cluster API

Adds the following initial CRUD endpoints for Clusters API:

- GET list of clusters
- GET specific cluster
- POST add existing cluster (mimic of "Add cluster")
- PUT update cluser
- DELETE destroy cluster

Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/40473
parent 873c0b53
---
title: Add API Support for Kubernetes integration
merge_request: 23922
author:
type: added
......@@ -139,6 +139,7 @@ module API
mount ::API::Pipelines
mount ::API::PipelineSchedules
mount ::API::ProjectApprovals
mount ::API::ProjectClusters
mount ::API::ProjectExport
mount ::API::ProjectImport
mount ::API::ProjectHooks
......
......@@ -1565,6 +1565,39 @@ module API
expose :from_content
expose :to_content
end
module Platform
class Kubernetes < Grape::Entity
expose :api_url
expose :namespace
expose :authorization_type
expose :ca_cert
end
end
module Provider
class Gcp < Grape::Entity
expose :cluster_id
expose :status_name
expose :gcp_project_id
expose :zone
expose :machine_type
expose :num_nodes
expose :endpoint
end
end
class Cluster < Grape::Entity
expose :id, :name, :created_at
expose :provider_type, :platform_type, :environment_scope, :cluster_type
expose :user, using: Entities::UserBasic
expose :platform_kubernetes, using: Entities::Platform::Kubernetes
expose :provider, as: :gcp_provider, using: Entities::Provider::Gcp, if: ->(cluster, _) { cluster.gcp? }
end
class ClusterProject < Cluster
expose :project, using: Entities::BasicProjectDetails
end
end
end
......
# frozen_string_literal: true
module API
class ProjectClusters < Grape::API
include PaginationParams
before { authenticate! }
helpers do
params :optional_add_params_ee do
# EE::API::ProjectClusters would override this
end
# EE::API::ProjectClusters would override this
def environment_scope
'*'
end
end
prepend EE::API::ProjectClusters
params do
requires :id, type: String, desc: 'The ID of the project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all clusters from the project' do
detail 'This feature was introduced in GitLab 11.7.'
success Entities::Cluster
end
params do
use :pagination
end
get ':id/clusters' do
authorize! :read_cluster, user_project
present paginate(clusters_for_current_user), with: Entities::Cluster
end
desc 'Get specific cluster for the project' do
detail 'This feature was introduced in GitLab 11.7.'
success Entities::ClusterProject
end
params do
requires :cluster_id, type: Integer, desc: 'The cluster ID'
end
get ':id/clusters/:cluster_id' do
authorize! :read_cluster, cluster
present cluster, with: Entities::ClusterProject
end
desc 'Adds an existing cluster' do
detail 'This feature was introduced in GitLab 11.7.'
success Entities::ClusterProject
end
params do
requires :name, type: String, desc: 'Cluster name'
requires :api_url, type: String, desc: 'URL to access the Kubernetes API'
requires :token, type: String, desc: 'Token to authenticate against Kubernetes'
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
optional :namespace, type: String, desc: 'Unique namespace related to Project'
optional :rbac_enabled, type: Boolean, default: true, desc: 'Enable RBAC authorization type, defaults to true'
use :optional_add_params_ee
end
post ':id/add_cluster' do
authorize! :create_cluster, user_project
new_cluster = ::Clusters::CreateService
.new(current_user, create_cluster_params)
.execute(access_token: token_in_session)
.present(current_user: current_user)
if new_cluster.persisted?
present new_cluster, with: Entities::ClusterProject
else
render_validation_error!(new_cluster)
end
end
desc 'Update an existing cluster' do
detail 'This feature was introduced in GitLab 11.7.'
success Entities::ClusterProject
end
params do
requires :cluster_id, type: Integer, desc: 'The cluster ID'
optional :name, type: String, desc: 'Cluster name'
optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
optional :token, type: String, desc: 'Token to authenticate against Kubernetes'
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
optional :namespace, type: String, desc: 'Unique namespace related to Project'
end
put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster
update_service = Clusters::UpdateService.new(current_user, update_cluster_params)
if update_service.execute(cluster)
present cluster, with: Entities::ClusterProject
else
render_validation_error!(cluster)
end
end
desc 'Remove a cluster' do
detail 'This feature was introduced in GitLab 11.7.'
success Entities::ClusterProject
end
params do
requires :cluster_id, type: Integer, desc: 'The Cluster ID'
end
delete ':id/clusters/:cluster_id' do
authorize! :admin_cluster, cluster
destroy_conditionally!(cluster)
end
end
helpers do
def clusters_for_current_user
@clusters_for_current_user ||= ClustersFinder.new(user_project, current_user, :all).execute
end
def cluster
@cluster ||= clusters_for_current_user.find(params[:cluster_id])
end
def token_in_session
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end
def create_cluster_params
{
name: declared_params[:name],
enabled: true,
environment_scope: environment_scope,
provider_type: :user,
platform_type: :kubernetes,
cluster_type: :project,
clusterable: user_project,
platform_kubernetes_attributes: create_platform_kubernetes_params
}
end
def create_platform_kubernetes_params
kubernetes_params = { authorization_type: kubernetes_authorization_type }
permitted_params = platform_kubernetes_params + [:authorization_type]
kubernetes_params.merge(declared_params.slice(*permitted_params))
end
def update_cluster_params
{
platform_kubernetes_attributes: update_platform_kubernetes_params
}.merge(cluster.managed? ? {} : { name: declared_params[:name] })
end
def update_platform_kubernetes_params
permitted_params = if cluster.managed?
[:namespace]
else
platform_kubernetes_params
end
declared_params.slice(*permitted_params)
end
def platform_kubernetes_params
[:api_url, :token, :ca_cert, :namespace]
end
def kubernetes_authorization_type
rbac_enabled = declared_params.fetch(:rbac_enabled, true)
rbac_enabled ? authorization_types[:rbac] : authorization_types[:abac]
end
def authorization_types
Clusters::Platforms::Kubernetes.authorization_types
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe API::ProjectClusters do
include KubernetesHelpers
let(:current_user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { create(:project, :repository) }
before do
project.add_maintainer(current_user)
end
describe 'GET /projects/:id/clusters' do
let!(:extra_cluster) { create(:cluster, :provided_by_gcp, :project) }
let!(:clusters) do
create_list(:cluster, 5, :provided_by_gcp, :project, :production_environment,
projects: [project])
end
context 'non-authorized user' do
it 'should respond with 404' do
get api("/projects/#{project.id}/clusters", non_member)
expect(response).to have_gitlab_http_status(404)
end
end
context 'authorized user' do
before do
get api("/projects/#{project.id}/clusters", current_user)
end
it 'should respond with 200' do
expect(response).to have_gitlab_http_status(200)
end
it 'should include pagination headers' do
expect(response).to include_pagination_headers
end
it 'should only include authorized clusters' do
cluster_ids = json_response.map { |cluster| cluster['id'] }
expect(cluster_ids).to match_array(clusters.pluck(:id))
expect(cluster_ids).not_to include(extra_cluster.id)
end
end
end
describe 'GET /projects/:id/clusters/:cluster_id' do
let(:cluster_id) { cluster.id }
let(:platform_kubernetes) do
create(:cluster_platform_kubernetes, :configured,
namespace: 'project-namespace')
end
let(:cluster) do
create(:cluster, :project, :provided_by_gcp,
platform_kubernetes: platform_kubernetes,
user: current_user,
projects: [project])
end
context 'non-authorized user' do
it 'should respond with 404' do
get api("/projects/#{project.id}/clusters/#{cluster_id}", non_member)
expect(response).to have_gitlab_http_status(404)
end
end
context 'authorized user' do
before do
get api("/projects/#{project.id}/clusters/#{cluster_id}", current_user)
end
it 'returns specific cluster' do
expect(json_response['id']).to eq(cluster.id)
end
it 'returns cluster information' do
expect(json_response['provider_type']).to eq('gcp')
expect(json_response['platform_type']).to eq('kubernetes')
expect(json_response['environment_scope']).to eq('*')
expect(json_response['cluster_type']).to eq('project_type')
end
it 'returns project information' do
cluster_project = json_response['project']
expect(cluster_project['id']).to eq(project.id)
expect(cluster_project['name']).to eq(project.name)
expect(cluster_project['path']).to eq(project.path)
end
it 'returns kubernetes platform information' do
platform = json_response['platform_kubernetes']
expect(platform['api_url']).to eq('https://kubernetes.example.com')
expect(platform['namespace']).to eq('project-namespace')
expect(platform['ca_cert']).to be_present
end
it 'returns user information' do
user = json_response['user']
expect(user['id']).to eq(current_user.id)
expect(user['username']).to eq(current_user.username)
end
it 'returns GCP provider information' do
gcp_provider = json_response['gcp_provider']
expect(gcp_provider['cluster_id']).to eq(cluster.id)
expect(gcp_provider['status_name']).to eq('created')
expect(gcp_provider['gcp_project_id']).to eq('test-gcp-project')
expect(gcp_provider['zone']).to eq('us-central1-a')
expect(gcp_provider['machine_type']).to eq('n1-standard-2')
expect(gcp_provider['num_nodes']).to eq(3)
expect(gcp_provider['endpoint']).to eq('111.111.111.111')
end
context 'when cluster has no provider' do
let(:cluster) do
create(:cluster, :project, :provided_by_user,
projects: [project])
end
it 'should not include GCP provider info' do
expect(json_response['gcp_provider']).not_to be_present
end
end
context 'for non existing cluster' do
let(:cluster_id) { 123 }
it 'returns 404' do
expect(response).to have_gitlab_http_status(404)
end
end
end
end
shared_context 'kubernetes calls stubbed' do
before do
stub_kubeclient_discover(api_url)
stub_kubeclient_get_namespace(api_url, namespace: namespace)
stub_kubeclient_get_service_account(api_url, "#{namespace}-service-account", namespace: namespace)
stub_kubeclient_put_service_account(api_url, "#{namespace}-service-account", namespace: namespace)
stub_kubeclient_get_secret(
api_url,
{
metadata_name: "#{namespace}-token",
token: Base64.encode64('sample-token'),
namespace: namespace
}
)
stub_kubeclient_put_secret(api_url, "#{namespace}-token", namespace: namespace)
stub_kubeclient_get_role_binding(api_url, "gitlab-#{namespace}", namespace: namespace)
stub_kubeclient_put_role_binding(api_url, "gitlab-#{namespace}", namespace: namespace)
end
end
describe 'POST /projects/:id/add_cluster' do
include_context 'kubernetes calls stubbed'
let(:api_url) { 'https://kubernetes.example.com' }
let(:namespace) { project.path }
let(:base_params) do
{
name: 'test-cluster',
api_url: api_url,
token: 'sample-token',
namespace: namespace
}
end
context 'non-authorized user' do
it 'should respond with 404' do
post api("/projects/#{project.id}/add_cluster", non_member), params: base_params
expect(response).to have_gitlab_http_status(404)
end
end
context 'authorized user' do
before do
post api("/projects/#{project.id}/add_cluster", current_user), params: cluster_params
end
context 'with valid params' do
let(:cluster_params) { base_params }
it 'should respond with 201' do
expect(response).to have_gitlab_http_status(201)
end
it 'should create a new Cluster::Cluster' do
cluster_result = Clusters::Cluster.find(json_response["id"])
platform_kubernetes = cluster_result.platform
expect(cluster_result).to be_user
expect(cluster_result).to be_kubernetes
expect(cluster_result.project).to eq(project)
expect(cluster_result.name).to eq('test-cluster')
expect(platform_kubernetes.rbac?).to be_truthy
expect(platform_kubernetes.api_url).to eq(api_url)
expect(platform_kubernetes.namespace).to eq(namespace)
expect(platform_kubernetes.token).to eq('sample-token')
end
end
context 'when user disables RBAC' do
let(:cluster_params) { base_params.merge(rbac_enabled: false) }
it 'should create an ABAC cluster' do
cluster_result = Clusters::Cluster.find(json_response["id"])
expect(cluster_result.platform.abac?).to be_truthy
end
end
context 'with invalid params' do
let(:cluster_params) { base_params.merge(name: '') }
it 'should respond with 400' do
expect(response).to have_gitlab_http_status(400)
end
it 'should not create a new Clusters::Cluster' do
expect(project.reload.clusters).to be_empty
end
it 'should return validation errors' do
expect(json_response['message']).to be_present
expect(json_response['message']['name'].first).to eq(" has to be present")
end
end
end
end
describe 'PUT /projects/:id/clusters/:cluster_id' do
include_context 'kubernetes calls stubbed'
let(:api_url) { 'https://kubernetes.example.com' }
let(:namespace) { 'new-namespace' }
let(:base_params) { { namespace: namespace } }
let(:update_params) { base_params }
let!(:kubernetes_namespace) do
create(:cluster_kubernetes_namespace,
cluster: cluster,
project: project)
end
let(:cluster) do
create(:cluster, :project, :provided_by_gcp,
projects: [project])
end
context 'non-authorized user' do
it 'should respond with 404' do
put api("/projects/#{project.id}/clusters/#{cluster.id}", non_member), params: update_params
expect(response).to have_gitlab_http_status(404)
end
end
context 'authorized user' do
before do
put api("/projects/#{project.id}/clusters/#{cluster.id}", current_user), params: update_params
cluster.reload
end
it 'should respond with 200' do
expect(response).to have_gitlab_http_status(200)
end
context 'with valid params' do
it 'should update cluster attributes' do
expect(cluster.platform_kubernetes.namespace).to eq('new-namespace')
expect(cluster.kubernetes_namespace.namespace).to eq('new-namespace')
end
end
context 'with invalid params' do
let(:namespace) { 'invalid_namespace' }
it 'should not update cluster attributes' do
expect(cluster.platform_kubernetes.namespace).not_to eq('invalid_namespace')
expect(cluster.kubernetes_namespace.namespace).not_to eq('invalid_namespace')
end
it 'should return validation errors' do
expect(json_response['message']).to be_present
expect(json_response['message']['platform_kubernetes.namespace'].first).to match('can contain only lowercase letters')
end
end
context 'with a GCP cluster' do
let(:update_params) do
base_params.merge(
name: 'new-name',
api_url: 'https://new-api-url.com',
token: 'new-sample-token'
)
end
it 'should ignore platform kubernetes specific attributes' do
platform_kubernetes = cluster.platform_kubernetes
expect(cluster.name).not_to eq('new-name')
expect(platform_kubernetes.namespace).to eq('new-namespace')
expect(platform_kubernetes.api_url).not_to eq('https://new-api-url.com')
expect(platform_kubernetes.token).not_to eq('new-sample-token')
end
end
context 'with an user cluster' do
let(:api_url) { 'https://new-api-url.com' }
let(:cluster) do
create(:cluster, :project, :provided_by_user,
projects: [project])
end
let(:update_params) do
base_params.merge(
name: 'new-name',
api_url: 'https://new-api-url.com',
token: 'new-sample-token'
)
end
it 'should not ignore platform kubernetes specific attributes' do
platform_kubernetes = cluster.platform_kubernetes
expect(cluster.name).to eq('new-name')
expect(platform_kubernetes.namespace).to eq('new-namespace')
expect(platform_kubernetes.api_url).to eq('https://new-api-url.com')
expect(platform_kubernetes.token).to eq('new-sample-token')
end
end
context 'with a cluster that does not belong to user' do
let(:cluster) { create(:cluster, :project, :provided_by_user) }
it 'should respond with 400' do
expect(response).to have_gitlab_http_status(404)
end
end
end
end
describe 'DELETE /projects/:id/clusters/:cluster_id' do
let(:cluster_params) { { cluster_id: cluster.id } }
let(:cluster) do
create(:cluster, :project, :provided_by_gcp,
projects: [project])
end
context 'non-authorized user' do
it 'should respond with 404' do
delete api("/projects/#{project.id}/clusters/#{cluster.id}", non_member), params: cluster_params
expect(response).to have_gitlab_http_status(404)
end
end
context 'authorized user' do
before do
delete api("/projects/#{project.id}/clusters/#{cluster.id}", current_user), params: cluster_params
end
it 'should respond with 204' do
expect(response).to have_gitlab_http_status(204)
end
it 'should delete the cluster' do
expect(Clusters::Cluster.exists?(id: cluster.id)).to be_falsy
end
context 'with a cluster that does not belong to user' do
let(:cluster) { create(:cluster, :project, :provided_by_user) }
it 'should respond with 404' do
expect(response).to have_gitlab_http_status(404)
end
end
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