Commit e4254f4a authored by Thong Kuah's avatar Thong Kuah

New cluster environments endpoint

- GitLab Premium feature. Adds cluster_deployments license feature

- Finder for Cluster's environments. Returns a scope for the cluster's
environments. If no permission, returns a None scope

- Add a new serializer to represent environments for a cluster. These
can be for a group cluster so we cannot restrict to a single project
(like EnvironmentEntity currently does)

- Load environments for a cluster. We need it to return where the
environment's latest deployment is to a cluster. (and not the latest
deployment to that cluster).

- Add n+1 request spec.
parent 80c5e53a
......@@ -18,3 +18,5 @@ class Groups::ClustersController < Clusters::ClustersController
@group ||= find_routable!(Group, params[:group_id] || params[:id])
end
end
Groups::ClustersController.prepend_if_ee('EE::Groups::ClustersController')
# frozen_string_literal: true
module EE
module Groups
module ClustersController
def environments
respond_to do |format|
format.json do
environments = ::Clusters::EnvironmentsFinder.new(cluster, current_user).execute
render json: serialize_environments(
environments.preload_for_cluster_environment_entity,
request,
response
)
end
end
end
private
def serialize_environments(environments, request, response)
::Clusters::EnvironmentSerializer
.new(cluster: cluster, current_user: current_user)
.with_pagination(request, response)
.represent(environments)
end
end
end
end
# frozen_string_literal: true
module Clusters
class EnvironmentsFinder
def initialize(cluster, current_user)
@cluster = cluster
@current_user = current_user
end
def execute
if can_read_cluster_environments?
::Environment.available.deployed_to_cluster(cluster)
else
::Environment.none
end
end
private
attr_reader :cluster, :current_user
def can_read_cluster_environments?
Ability.allowed?(current_user, :read_cluster_environments, cluster)
end
end
end
......@@ -10,6 +10,21 @@ module EE
has_many :prometheus_alerts, inverse_of: :environment
has_one :last_deployable, through: :last_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_pipeline, through: :last_deployable, source: 'pipeline'
# Returns environments where its latest deployment is to a cluster
scope :deployed_to_cluster, -> (cluster) do
joins(:deployments)
.joins("LEFT OUTER JOIN deployments AS later_deployments ON later_deployments.environment_id = deployments.environment_id AND deployments.id < later_deployments.id")
.where("later_deployments.id IS NULL")
.where(deployments: { cluster_id: cluster.id })
end
scope :preload_for_cluster_environment_entity, -> do
preload(
last_deployment: [:deployable],
project: [:route, { namespace: :route }]
)
end
end
def reactive_cache_updated
......
......@@ -50,6 +50,7 @@ class License < ApplicationRecord
email_additional_text
db_load_balancing
deploy_board
cluster_deployments
extended_audit_events
file_locks
geo
......
......@@ -41,6 +41,10 @@ module EE
@subject.feature_available?(:dependency_proxy)
end
condition(:cluster_deployments_available) do
@subject.feature_available?(:cluster_deployments)
end
rule { reporter }.policy do
enable :admin_list
enable :admin_board
......@@ -55,6 +59,9 @@ module EE
enable :owner_access
end
rule { can?(:read_cluster) & cluster_deployments_available }
.enable :read_cluster_environments
rule { can?(:read_group) & contribution_analytics_available }
.enable :read_group_contribution_analytics
......
# frozen_string_literal: true
module Clusters
class DeploymentEntity < Grape::Entity
expose :id, :iid
expose :deployable do |deployment|
{ name: deployment.deployable.name }
end
end
end
# frozen_string_literal: true
module Clusters
class EnvironmentEntity < API::Entities::EnvironmentBasic
include RequestAwareEntity
expose :project, using: API::Entities::ProjectIdentity
expose :last_deployment, using: Clusters::DeploymentEntity
expose :environment_path do |environment|
project_environment_path(environment.project, environment)
end
expose :rollout_status, if: -> (*) { can_read_cluster_deployments? }, using: ::RolloutStatusEntity
private
alias_method :environment, :object
def current_user
request.current_user
end
def can_read_cluster_deployments?
can?(current_user, :read_cluster_environments, request.cluster)
end
end
end
# frozen_string_literal: true
module Clusters
class EnvironmentSerializer < BaseSerializer
include WithPagination
entity ::Clusters::EnvironmentEntity
end
end
---
title: Add internal API for group cluster environments
merge_request: 15096
author:
type: other
......@@ -31,6 +31,12 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
end
end
resources :clusters, only: [] do
member do
get :environments, format: :json
end
end
resource :ldap, only: [] do
member do
put :sync
......
......@@ -3,8 +3,17 @@
require 'spec_helper'
describe Groups::ClustersController do
include AccessMatchersForController
set(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_maintainer(user)
sign_in(user)
end
it_behaves_like 'cluster metrics' do
let(:clusterable) { group }
......@@ -19,4 +28,40 @@ describe Groups::ClustersController do
}
end
end
describe 'GET environments' do
let(:cluster) { create(:cluster_for_group, groups: [group]) }
before do
create(:deployment, :success, cluster: cluster)
end
def go
get :environments,
params: {
group_id: group.to_param,
id: cluster
},
format: :json
end
describe 'functionality' do
it 'responds successfully' do
go
expect(response).to have_gitlab_http_status(:ok)
end
end
describe 'security' do
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::EnvironmentsFinder, '#execute' do
let(:current_user) { create(:user) }
let(:last_deployment) { create(:deployment, :success, :on_cluster) }
let(:cluster) { last_deployment.cluster }
let(:environment) { last_deployment.environment }
before do
allow(Ability).to receive(:allowed?)
.with(current_user, :read_cluster_environments, cluster)
.and_return(allowed)
end
subject { described_class.new(cluster, current_user).execute }
context 'current_user can read cluster environments' do
let(:allowed) { true}
it { is_expected.to include(environment) }
context 'environment is not available' do
before do
environment.stop!
end
it { is_expected.not_to include(environment) }
end
end
context 'current_user cannot read cluster environments' do
let(:allowed) { false }
it { is_expected.to be_empty }
end
end
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"deployable": {
"oneOf": [
{ "type": "null" },
{ "$ref": "../../../../../../spec/fixtures/api/schemas/job/job.json" }
]
}
},
"additionalProperties": false
}
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"slug": { "type": "string" },
"external_url": {
"type": [
"string",
"null"
]
},
"environment_path": { "type": "string" },
"project": { "$ref": "../../../../../../spec/fixtures/api/schemas/public_api/v4/project/identity.json" },
"last_deployment": {
"oneOf": [
{ "type": "null" },
{ "$ref": "deployment.json" }
]
},
"rollout_status": {
"$ref": "../rollout_status.json"
}
},
"additionalProperties": false
}
......@@ -8,6 +8,55 @@ describe Environment, :use_clean_rails_memory_store_caching do
let(:project) { create(:project, :stubbed_repository) }
let(:environment) { create(:environment, project: project) }
describe '.deployed_to_cluster' do
let!(:environment) { create(:environment) }
context 'when there is no deployment' do
let(:cluster) { create(:cluster) }
it 'returns nothing' do
expect(described_class.deployed_to_cluster(cluster)).to be_empty
end
end
context 'when there is a deployment for the cluster' do
let(:cluster) { last_deployment.cluster }
let(:last_deployment) do
create(:deployment, :success, :on_cluster, environment: environment)
end
it 'returns the environment for the last deployment' do
expect(described_class.deployed_to_cluster(cluster)).to eq([environment])
end
end
context 'when there is a non-cluster deployment' do
let(:cluster) { create(:cluster) }
before do
create(:deployment, :success, environment: environment)
end
it 'returns nothing' do
expect(described_class.deployed_to_cluster(cluster)).to be_empty
end
end
context 'when the non-cluster deployment is latest' do
let(:cluster) { create(:cluster) }
before do
create(:deployment, :success, cluster: cluster, environment: environment)
create(:deployment, :success, environment: environment)
end
it 'returns nothing' do
expect(described_class.deployed_to_cluster(cluster)).to be_empty
end
end
end
describe '#pod_names' do
context 'when environment does not have a rollout status' do
it 'returns an empty array' do
......
......@@ -19,6 +19,26 @@ describe GroupPolicy do
it { is_expected.to be_allowed(:read_epic, :create_epic, :admin_epic, :destroy_epic) }
end
context 'when cluster deployments is available' do
let(:current_user) { maintainer }
before do
stub_licensed_features(cluster_deployments: true)
end
it { is_expected.to be_allowed(:read_cluster_environments) }
end
context 'when cluster deployments is not available' do
let(:current_user) { maintainer }
before do
stub_licensed_features(cluster_deployments: false)
end
it { is_expected.not_to be_allowed(:read_cluster_environments) }
end
context 'when contribution analytics is available' do
let(:current_user) { developer }
......
# frozen_string_literal: true
require 'spec_helper'
describe Groups::ClustersController do
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_maintainer(user)
login_as(user)
end
describe 'GET #environments' do
def go
get environments_group_cluster_path(group, cluster, format: :json)
end
let(:cluster) { create(:cluster_for_group, groups: [group]) }
before do
stub_licensed_features(cluster_deployments: true)
create(:deployment, :success, cluster: cluster)
end
it 'avoids N+1 database queries' do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { go }.count
create_list(:deployment, 2, :success, cluster: cluster)
# TODO remove this leeway when we refactor away from deployment_platform
# (https://gitlab.com/gitlab-org/gitlab-ee/issues/13635)
leeway = 5
expect { go }.not_to exceed_all_query_limit(control_count + leeway)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::DeploymentEntity do
let(:deployment) { create(:deployment) }
subject { described_class.new(deployment).as_json }
it 'exposes id' do
expect(subject).to include(:id)
end
it 'exposes iid' do
expect(subject).to include(:iid)
end
it 'exposes deployable name' do
expect(subject).to include(:deployable)
expect(subject[:deployable]).to include(:name)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::EnvironmentEntity do
set(:user) { create(:user) }
set(:group) { create(:group) }
set(:project) { create(:project, group: group) }
set(:cluster) { create(:cluster_for_group, groups: [group]) }
it 'inherits from API::Entities::EnvironmentBasic' do
expect(described_class).to be < API::Entities::EnvironmentBasic
end
describe '#as_json' do
let(:environment) { create(:environment, project: project) }
let(:request) { double('request', current_user: user, cluster: cluster) }
before do
group.add_maintainer(user)
end
subject { described_class.new(environment, request: request).as_json }
it 'exposes project' do
expect(subject).to include(:project)
end
it 'exposes last_deployment' do
expect(subject).to include(:last_deployment)
end
it 'exposes environment_path' do
expect(subject).to include(:environment_path)
end
context 'deploy board available' do
before do
allow(group).to receive(:feature_available?).and_call_original
allow(group).to receive(:feature_available?).with(:cluster_deployments).and_return(true)
end
it 'exposes rollout_status' do
expect(subject).to include(:rollout_status)
end
end
context 'deploy board not available' do
before do
allow(group).to receive(:feature_available?).with(:cluster_deployments).and_return(false)
end
it 'does not expose rollout_status' do
expect(subject).not_to include(:rollout_status)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::EnvironmentSerializer do
include KubernetesHelpers
set(:user) { create(:user) }
set(:project) { create(:project, namespace: user.namespace) }
set(:cluster) { create(:cluster) }
let(:resource) { create(:environment, project: project) }
let(:json_entity) do
described_class.new(cluster: cluster, current_user: user)
.represent(resource)
.with_indifferent_access
end
it 'matches clusters/environment json schema' do
expect(json_entity).to match_schema('clusters/environment', dir: 'ee')
end
end
# frozen_string_literal: true
require 'spec_helper'
describe EnvironmentEntity do
using RSpec::Parameterized::TableSyntax
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, :with_review_app, ref: 'development', project: project) }
......
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