clusters_controller.rb 9.88 KB
Newer Older
1 2
# frozen_string_literal: true

3 4
class Clusters::ClustersController < Clusters::BaseController
  include RoutableActions
5
  include Metrics::Dashboard::PrometheusApiProxy
6
  include MetricsDashboard
7

8
  before_action :cluster, only: [:cluster_status, :show, :update, :destroy, :clear_cache]
9 10
  before_action :generate_gcp_authorize_url, only: [:new]
  before_action :validate_gcp_token, only: [:new]
11 12
  before_action :gcp_cluster, only: [:new]
  before_action :user_cluster, only: [:new]
13
  before_action :authorize_create_cluster!, only: [:new, :authorize_aws_role]
14
  before_action :authorize_update_cluster!, only: [:update]
15
  before_action :authorize_admin_cluster!, only: [:destroy, :clear_cache]
16
  before_action :update_applications_status, only: [:cluster_status]
17

18
  helper_method :token_in_session
19

Kamil Trzcinski's avatar
Kamil Trzcinski committed
20 21
  STATUS_POLLING_INTERVAL = 10_000

22
  def index
Emily Ring's avatar
Emily Ring committed
23
    @clusters = cluster_list
24

Emily Ring's avatar
Emily Ring committed
25 26 27
    respond_to do |format|
      format.html
      format.json do
28
        Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL)
Emily Ring's avatar
Emily Ring committed
29
        serializer = ClusterSerializer.new(current_user: current_user)
30

Emily Ring's avatar
Emily Ring committed
31 32 33 34 35 36
        render json: {
          clusters: serializer.with_pagination(request, response).represent_list(@clusters),
          has_ancestor_clusters: @has_ancestor_clusters
        }
      end
    end
37 38
  end

39
  def new
40
    if params[:provider] == 'aws'
41
      @aws_role = Aws::Role.create_or_find_by!(user: current_user)
42
      @instance_types = load_instance_types.to_json
43

44
    elsif params[:provider] == 'gcp'
45 46
      redirect_to @authorize_url if @authorize_url && !@valid_gcp_token
    end
47 48
  end

49 50
  # Overridding ActionController::Metal#status is NOT a good idea
  def cluster_status
51 52
    respond_to do |format|
      format.json do
53
        Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL)
54

55
        render json: ClusterSerializer
56
          .new(current_user: @current_user)
57
          .represent_status(@cluster)
58 59
      end
    end
60 61
  end

62
  def show
63 64 65
  end

  def update
Shinya Maeda's avatar
Shinya Maeda committed
66
    Clusters::UpdateService
67
      .new(current_user, update_params)
68
      .execute(cluster)
69

Kamil Trzcinski's avatar
Kamil Trzcinski committed
70
    if cluster.valid?
71 72 73 74 75
      respond_to do |format|
        format.json do
          head :no_content
        end
        format.html do
76
          flash[:notice] = _('Kubernetes cluster was successfully updated.')
77
          redirect_to cluster.show_path
78 79
        end
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
80
    else
81 82 83 84
      respond_to do |format|
        format.json { head :bad_request }
        format.html { render :show }
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
85
    end
86 87
  end

88
  def destroy
89 90 91 92 93 94
    response = Clusters::DestroyService
      .new(current_user, destroy_params)
      .execute(cluster)

    flash[:notice] = response[:message]
    redirect_to clusterable.index_path, status: :found
95 96
  end

97 98
  def create_gcp
    @gcp_cluster = ::Clusters::CreateService
99
      .new(current_user, create_gcp_cluster_params)
100
      .execute(access_token: token_in_session)
101
      .present(current_user: current_user)
102 103

    if @gcp_cluster.persisted?
104
      redirect_to @gcp_cluster.show_path
105 106 107 108
    else
      generate_gcp_authorize_url
      validate_gcp_token
      user_cluster
109
      params[:provider] = 'gcp'
110

111
      render :new, locals: { active_tab: 'create' }
112 113 114
    end
  end

115 116 117 118 119 120 121 122 123 124 125 126 127
  def create_aws
    @aws_cluster = ::Clusters::CreateService
      .new(current_user, create_aws_cluster_params)
      .execute
      .present(current_user: current_user)

    if @aws_cluster.persisted?
      head :created, location: @aws_cluster.show_path
    else
      render status: :unprocessable_entity, json: @aws_cluster.errors
    end
  end

128 129
  def create_user
    @user_cluster = ::Clusters::CreateService
130
      .new(current_user, create_user_cluster_params)
131
      .execute(access_token: token_in_session)
132
      .present(current_user: current_user)
133 134

    if @user_cluster.persisted?
135
      redirect_to @user_cluster.show_path
136 137 138 139 140
    else
      generate_gcp_authorize_url
      validate_gcp_token
      gcp_cluster

141
      render :new, locals: { active_tab: 'add' }
142 143 144
    end
  end

145
  def authorize_aws_role
146 147
    response = Clusters::Aws::AuthorizeRoleService.new(
      current_user,
148
      params: aws_role_params
149
    ).execute
150

151
    render json: response.body, status: response.status
152 153
  end

154 155 156 157 158 159
  def clear_cache
    cluster.delete_cached_resources!

    redirect_to cluster.show_path, notice: _('Cluster cache cleared.')
  end

160 161
  private

Emily Ring's avatar
Emily Ring committed
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
  def cluster_list
    finder = ClusterAncestorsFinder.new(clusterable.subject, current_user)
    clusters = finder.execute

    @has_ancestor_clusters = finder.has_ancestor_clusters?

    # Note: We are paginating through an array here but this should OK as:
    #
    # In CE, we can have a maximum group nesting depth of 21, so including
    # project cluster, we can have max 22 clusters for a group hierarchy.
    # In EE (Premium) we can have any number, as multiple clusters are
    # supported, but the number of clusters are fairly low currently.
    #
    # See https://gitlab.com/gitlab-org/gitlab-foss/issues/55260 also.
    Kaminari.paginate_array(clusters).page(params[:page]).per(20)
  end

179
  def destroy_params
180
    params.permit(:cleanup)
181 182
  end

183 184 185 186 187 188 189 190 191
  def base_permitted_cluster_params
    [
      :enabled,
      :environment_scope,
      :managed,
      :namespace_per_environment
    ]
  end

192
  def update_params
193
    if cluster.provided_by_user?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
194
      params.require(:cluster).permit(
195
        *base_permitted_cluster_params,
196
        :name,
197
        :base_domain,
198
        :management_project_id,
Kamil Trzcinski's avatar
Kamil Trzcinski committed
199
        platform_kubernetes_attributes: [
200 201 202
          :api_url,
          :token,
          :ca_cert,
Kamil Trzcinski's avatar
Kamil Trzcinski committed
203 204 205 206 207
          :namespace
        ]
      )
    else
      params.require(:cluster).permit(
208
        *base_permitted_cluster_params,
209
        :base_domain,
210
        :management_project_id,
Kamil Trzcinski's avatar
Kamil Trzcinski committed
211 212
        platform_kubernetes_attributes: [
          :namespace
Kamil Trzcinski's avatar
Kamil Trzcinski committed
213
        ]
Kamil Trzcinski's avatar
Kamil Trzcinski committed
214 215
      )
    end
216 217
  end

218 219
  def create_gcp_cluster_params
    params.require(:cluster).permit(
220
      *base_permitted_cluster_params,
221 222 223 224 225
      :name,
      provider_gcp_attributes: [
        :gcp_project_id,
        :zone,
        :num_nodes,
226
        :machine_type,
227
        :cloud_run,
228
        :legacy_abac
229 230
      ]).merge(
        provider_type: :gcp,
231
        platform_type: :kubernetes,
232
        clusterable: clusterable.subject
233 234 235 236 237
      )
  end

  def create_aws_cluster_params
    params.require(:cluster).permit(
238
      *base_permitted_cluster_params,
239 240
      :name,
      provider_aws_attributes: [
241
        :kubernetes_version,
242 243 244 245 246 247 248 249 250 251 252 253
        :key_name,
        :role_arn,
        :region,
        :vpc_id,
        :instance_type,
        :num_nodes,
        :security_group_id,
        subnet_ids: []
      ]).merge(
        provider_type: :aws,
        platform_type: :kubernetes,
        clusterable: clusterable.subject
254 255 256 257 258
      )
  end

  def create_user_cluster_params
    params.require(:cluster).permit(
259
      *base_permitted_cluster_params,
260 261 262 263 264
      :name,
      platform_kubernetes_attributes: [
        :namespace,
        :api_url,
        :token,
265 266
        :ca_cert,
        :authorization_type
267 268
      ]).merge(
        provider_type: :user,
269
        platform_type: :kubernetes,
270
        clusterable: clusterable.subject
271 272 273
      )
  end

274
  def aws_role_params
275
    params.require(:cluster).permit(:role_arn, :region)
276 277
  end

278
  def generate_gcp_authorize_url
279
    state = generate_session_key_redirect(clusterable.new_path(provider: :gcp).to_s)
280 281 282 283 284 285 286 287 288

    @authorize_url = GoogleApi::CloudPlatform::Client.new(
      nil, callback_google_api_auth_url,
      state: state).authorize_url
  rescue GoogleApi::Auth::ConfigMissingError
    # no-op
  end

  def gcp_cluster
289 290 291
    cluster = Clusters::BuildService.new(clusterable.subject).execute
    cluster.build_provider_gcp
    @gcp_cluster = cluster.present(current_user: current_user)
292 293
  end

294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
  def proxyable
    cluster.cluster
  end

  # During first iteration of dashboard variables implementation
  # cluster health case was omitted. Existing service for now is tied to
  # environment, which is not always present for cluster health dashboard.
  # It is planned to break coupling to environment https://gitlab.com/gitlab-org/gitlab/-/issues/213833.
  # It is also planned to move cluster health to metrics dashboard section https://gitlab.com/gitlab-org/gitlab/-/issues/220214
  # but for now I've used dummy class to stub variable substitution service, as there are no variables
  # in cluster health dashboard
  def proxy_variable_substitution_service
    @empty_service ||= Class.new(BaseService) do
      def initialize(proxyable, params)
        @proxyable, @params = proxyable, params
      end

      def execute
        success(params: @params)
      end
    end
  end

317
  def user_cluster
318 319 320
    cluster = Clusters::BuildService.new(clusterable.subject).execute
    cluster.build_platform_kubernetes
    @user_cluster = cluster.present(current_user: current_user)
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
  end

  def validate_gcp_token
    @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
      .validate_token(expires_at_in_session)
  end

  def token_in_session
    session[GoogleApi::CloudPlatform::Client.session_key_for_token]
  end

  def expires_at_in_session
    @expires_at_in_session ||=
      session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
  end

  def generate_session_key_redirect(uri)
    GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
      session[key] = uri
    end
  end

343 344 345 346 347 348 349 350 351 352 353 354 355
  ##
  # Unfortunately the EC2 API doesn't provide a list of
  # possible instance types. There is a workaround, using
  # the Pricing API, but instead of requiring the
  # user to grant extra permissions for this we use the
  # values that validate the CloudFormation template.
  def load_instance_types
    stack_template = File.read(Rails.root.join('vendor', 'aws', 'cloudformation', 'eks_cluster.yaml'))
    instance_types = YAML.safe_load(stack_template).dig('Parameters', 'NodeInstanceType', 'AllowedValues')

    instance_types.map { |type| Hash(name: type, value: type) }
  end

356 357
  def update_applications_status
    @cluster.applications.each(&:schedule_status_update)
358
  end
359
end
360

361
Clusters::ClustersController.prepend_if_ee('EE::Clusters::ClustersController')