Commit 1ce0e5a7 authored by Tiger's avatar Tiger

Add controller action for creating cluster AWS roles

Once hooked up to the Vue app, this will take care of
the first step of creating a cluster on EKS - capturing
the role GitLab will assume to create resources in the
user's account.

Adds four new options to ApplicationSetting:

* eks_integration_enabled
* eks_account_id
* eks_access_key_id
* eks_secret_access_key

:eks_account_id is exposed to the frontend to allow
users to add it as a trusted entity to their chosen
IAM role.
parent e1357145
...@@ -216,6 +216,10 @@ class ApplicationController < ActionController::Base ...@@ -216,6 +216,10 @@ class ApplicationController < ActionController::Base
end end
end end
def respond_201
head :created
end
def respond_422 def respond_422
head :unprocessable_entity head :unprocessable_entity
end end
......
...@@ -3,12 +3,12 @@ ...@@ -3,12 +3,12 @@
class Clusters::ClustersController < Clusters::BaseController class Clusters::ClustersController < Clusters::BaseController
include RoutableActions include RoutableActions
before_action :cluster, except: [:index, :new, :create_gcp, :create_user] before_action :cluster, except: [:index, :new, :create_gcp, :create_user, :authorize_aws_role]
before_action :generate_gcp_authorize_url, only: [:new] before_action :generate_gcp_authorize_url, only: [:new]
before_action :validate_gcp_token, only: [:new] before_action :validate_gcp_token, only: [:new]
before_action :gcp_cluster, only: [:new] before_action :gcp_cluster, only: [:new]
before_action :user_cluster, only: [:new] before_action :user_cluster, only: [:new]
before_action :authorize_create_cluster!, only: [:new] before_action :authorize_create_cluster!, only: [:new, :authorize_aws_role]
before_action :authorize_update_cluster!, only: [:update] before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy] before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:cluster_status] before_action :update_applications_status, only: [:cluster_status]
...@@ -43,10 +43,16 @@ class Clusters::ClustersController < Clusters::BaseController ...@@ -43,10 +43,16 @@ class Clusters::ClustersController < Clusters::BaseController
def new def new
return unless Feature.enabled?(:create_eks_clusters) return unless Feature.enabled?(:create_eks_clusters)
@gke_selected = params[:provider] == 'gke' if params[:provider] == 'eks'
@eks_selected = params[:provider] == 'eks' @eks_selected = true
@aws_role = current_user.aws_role || Aws::Role.new
@aws_role.ensure_role_external_id!
return redirect_to @authorize_url if @gke_selected && @authorize_url && !@valid_gcp_token elsif params[:provider] == 'gke'
@gke_selected = true
redirect_to @authorize_url if @authorize_url && !@valid_gcp_token
end
end end
# Overridding ActionController::Metal#status is NOT a good idea # Overridding ActionController::Metal#status is NOT a good idea
...@@ -132,6 +138,12 @@ class Clusters::ClustersController < Clusters::BaseController ...@@ -132,6 +138,12 @@ class Clusters::ClustersController < Clusters::BaseController
end end
end end
def authorize_aws_role
role = current_user.build_aws_role(create_role_params)
role.save ? respond_201 : respond_422
end
private private
def update_params def update_params
...@@ -203,6 +215,10 @@ class Clusters::ClustersController < Clusters::BaseController ...@@ -203,6 +215,10 @@ class Clusters::ClustersController < Clusters::BaseController
) )
end end
def create_role_params
params.require(:cluster).permit(:role_arn, :role_external_id)
end
def generate_gcp_authorize_url def generate_gcp_authorize_url
params = Feature.enabled?(:create_eks_clusters) ? { provider: :gke } : {} params = Feature.enabled?(:create_eks_clusters) ? { provider: :gke } : {}
state = generate_session_key_redirect(clusterable.new_path(params).to_s) state = generate_session_key_redirect(clusterable.new_path(params).to_s)
......
...@@ -193,6 +193,10 @@ module ApplicationSettingsHelper ...@@ -193,6 +193,10 @@ module ApplicationSettingsHelper
:dsa_key_restriction, :dsa_key_restriction,
:ecdsa_key_restriction, :ecdsa_key_restriction,
:ed25519_key_restriction, :ed25519_key_restriction,
:eks_integration_enabled,
:eks_account_id,
:eks_access_key_id,
:eks_secret_access_key,
:email_author_in_body, :email_author_in_body,
:enabled_git_access_protocol, :enabled_git_access_protocol,
:enforce_terms, :enforce_terms,
......
...@@ -274,6 +274,22 @@ class ApplicationSetting < ApplicationRecord ...@@ -274,6 +274,22 @@ class ApplicationSetting < ApplicationRecord
presence: true, presence: true,
if: :lets_encrypt_terms_of_service_accepted? if: :lets_encrypt_terms_of_service_accepted?
validates :eks_integration_enabled,
inclusion: { in: [true, false] }
validates :eks_account_id,
format: { with: Gitlab::Regex.aws_account_id_regex,
message: Gitlab::Regex.aws_account_id_message },
if: -> (setting) { setting.eks_integration_enabled? }
validates :eks_access_key_id,
length: { in: 16..128 },
if: -> (setting) { setting.eks_integration_enabled? }
validates :eks_secret_access_key,
presence: true,
if: -> (setting) { setting.eks_integration_enabled? }
validates_with X509CertificateCredentialsValidator, validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert, certificate: :external_auth_client_cert,
pkey: :external_auth_client_key, pkey: :external_auth_client_key,
...@@ -304,6 +320,12 @@ class ApplicationSetting < ApplicationRecord ...@@ -304,6 +320,12 @@ class ApplicationSetting < ApplicationRecord
algorithm: 'aes-256-gcm', algorithm: 'aes-256-gcm',
encode: true encode: true
attr_encrypted :eks_secret_access_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-gcm',
encode: true
before_validation :ensure_uuid! before_validation :ensure_uuid!
before_save :ensure_runners_registration_token before_save :ensure_runners_registration_token
......
...@@ -54,6 +54,10 @@ module ApplicationSettingImplementation ...@@ -54,6 +54,10 @@ module ApplicationSettingImplementation
dsa_key_restriction: 0, dsa_key_restriction: 0,
ecdsa_key_restriction: 0, ecdsa_key_restriction: 0,
ed25519_key_restriction: 0, ed25519_key_restriction: 0,
eks_integration_enabled: false,
eks_account_id: nil,
eks_access_key_id: nil,
eks_secret_access_key: nil,
first_day_of_week: 0, first_day_of_week: 0,
gitaly_timeout_default: 55, gitaly_timeout_default: 55,
gitaly_timeout_fast: 10, gitaly_timeout_fast: 10,
......
...@@ -13,5 +13,11 @@ module Aws ...@@ -13,5 +13,11 @@ module Aws
with: Gitlab::Regex.aws_arn_regex, with: Gitlab::Regex.aws_arn_regex,
message: Gitlab::Regex.aws_arn_regex_message message: Gitlab::Regex.aws_arn_regex_message
} }
before_validation :ensure_role_external_id!, if: :new_record?
def ensure_role_external_id!
self.role_external_id ||= SecureRandom.hex(20)
end
end end
end end
...@@ -29,6 +29,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated ...@@ -29,6 +29,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
new_polymorphic_path([clusterable, :cluster], options) new_polymorphic_path([clusterable, :cluster], options)
end end
def authorize_aws_role_path
polymorphic_path([clusterable, :clusters], action: :authorize_aws_role)
end
def create_user_clusters_path def create_user_clusters_path
polymorphic_path([clusterable, :clusters], action: :create_user) polymorphic_path([clusterable, :clusters], action: :create_user)
end end
......
...@@ -52,6 +52,11 @@ class InstanceClusterablePresenter < ClusterablePresenter ...@@ -52,6 +52,11 @@ class InstanceClusterablePresenter < ClusterablePresenter
create_gcp_admin_clusters_path create_gcp_admin_clusters_path
end end
override :authorize_aws_role_path
def authorize_aws_role_path
authorize_aws_role_admin_clusters_path
end
override :empty_state_help_text override :empty_state_help_text
def empty_state_help_text def empty_state_help_text
s_('ClusterIntegration|Adding an integration will share the cluster across all projects.') s_('ClusterIntegration|Adding an integration will share the cluster across all projects.')
......
...@@ -36,20 +36,12 @@ module Clusters ...@@ -36,20 +36,12 @@ module Clusters
::Aws::Credentials.new(access_key_id, secret_access_key) ::Aws::Credentials.new(access_key_id, secret_access_key)
end end
##
# This setting is not yet configurable or documented as these
# services are not currently used. This will be addressed in
# https://gitlab.com/gitlab-org/gitlab/merge_requests/18307
def access_key_id def access_key_id
Gitlab.config.kubernetes.provisioners.aws.access_key_id Gitlab::CurrentSettings.eks_access_key_id
end end
##
# This setting is not yet configurable or documented as these
# services are not currently used. This will be addressed in
# https://gitlab.com/gitlab-org/gitlab/merge_requests/18307
def secret_access_key def secret_access_key
Gitlab.config.kubernetes.provisioners.aws.secret_access_key Gitlab::CurrentSettings.eks_secret_access_key
end end
def session_name def session_name
......
- expanded = integration_expanded?('eks_')
%section.settings.as-eks.no-animate#js-eks-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Amazon EKS')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Amazon EKS integration allows you to provision EKS clusters from GitLab.')
.settings-content
= form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-eks-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.form-check
= f.check_box :eks_integration_enabled, class: 'form-check-input'
= f.label :eks_integration_enabled, class: 'form-check-label' do
Enable Amazon EKS integration
.form-group
= f.label :eks_account_id, 'Account ID', class: 'label-bold'
= f.text_field :eks_account_id, class: 'form-control'
.form-group
= f.label :eks_access_key_id, 'Access key ID', class: 'label-bold'
= f.text_field :eks_access_key_id, class: 'form-control'
.form-group
= f.label :eks_secret_access_key, 'Secret access key', class: 'label-bold'
= f.password_field :eks_secret_access_key, value: @application_setting.eks_secret_access_key, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
...@@ -8,4 +8,4 @@ ...@@ -8,4 +8,4 @@
= render 'admin/application_settings/third_party_offers' = render 'admin/application_settings/third_party_offers'
= render 'admin/application_settings/snowplow' = render 'admin/application_settings/snowplow'
= render_if_exists 'admin/application_settings/pendo' = render_if_exists 'admin/application_settings/pendo'
= render 'admin/application_settings/eks' if Feature.enabled?(:create_eks_clusters)
.js-create-eks-cluster-form-container{ data: { 'gitlab-managed-cluster-help-path' => help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'), - if !Gitlab::CurrentSettings.eks_integration_enabled?
'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index') } } - documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("integration/amazon") }
= s_('Amazon authentication is not %{link_start}property configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: '<a/>'.html_safe }
- else
.js-create-eks-cluster-form-container{ data: { 'gitlab-managed-cluster-help-path' => help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'),
'create-role-path' => clusterable.authorize_aws_role_path,
'account-id' => Gitlab::CurrentSettings.eks_account_id,
'external-id' => @aws_role.role_external_id,
'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index'),
'valid-credentials' => @aws_role.role_arn.present? } }
---
title: Add ApplicationSetting entries for EKS integration
merge_request: 18307
author:
type: other
...@@ -142,6 +142,7 @@ Rails.application.routes.draw do ...@@ -142,6 +142,7 @@ Rails.application.routes.draw do
collection do collection do
post :create_user post :create_user
post :create_gcp post :create_gcp
post :authorize_aws_role
end end
member do member do
......
# frozen_string_literal: true
class AddEksCredentialsToApplicationSettings < ActiveRecord::Migration[5.2]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
add_column :application_settings, :eks_integration_enabled, :boolean, null: false, default: false
add_column :application_settings, :eks_account_id, :string, limit: 128
add_column :application_settings, :eks_access_key_id, :string, limit: 128
add_column :application_settings, :encrypted_eks_secret_access_key_iv, :string, limit: 255
add_column :application_settings, :encrypted_eks_secret_access_key, :text
end
end
...@@ -345,6 +345,11 @@ ActiveRecord::Schema.define(version: 2019_11_05_094625) do ...@@ -345,6 +345,11 @@ ActiveRecord::Schema.define(version: 2019_11_05_094625) do
t.boolean "pendo_enabled", default: false, null: false t.boolean "pendo_enabled", default: false, null: false
t.string "pendo_url", limit: 255 t.string "pendo_url", limit: 255
t.integer "deletion_adjourned_period", default: 7, null: false t.integer "deletion_adjourned_period", default: 7, null: false
t.boolean "eks_integration_enabled", default: false, null: false
t.string "eks_account_id", limit: 128
t.string "eks_access_key_id", limit: 128
t.string "encrypted_eks_secret_access_key_iv", limit: 255
t.text "encrypted_eks_secret_access_key"
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id" t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id" t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id" t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
......
...@@ -212,6 +212,10 @@ are listed in the descriptions of the relevant settings. ...@@ -212,6 +212,10 @@ are listed in the descriptions of the relevant settings.
| `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. | | `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. |
| `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. | | `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. |
| `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. | | `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. |
| `eks_integration_enabled` | boolean | no | Enable integration with Amazon EKS |
| `eks_account_id` | string | no | Amazon account ID |
| `eks_access_key_id` | string | no | AWS IAM access key ID |
| `eks_secret_access_key` | string | no | AWS IAM secret access key |
| `elasticsearch_aws_access_key` | string | no | **(PREMIUM)** AWS IAM access key | | `elasticsearch_aws_access_key` | string | no | **(PREMIUM)** AWS IAM access key |
| `elasticsearch_aws` | boolean | no | **(PREMIUM)** Enable the use of AWS hosted Elasticsearch | | `elasticsearch_aws` | boolean | no | **(PREMIUM)** Enable the use of AWS hosted Elasticsearch |
| `elasticsearch_aws_region` | string | no | **(PREMIUM)** The AWS region the Elasticsearch domain is configured | | `elasticsearch_aws_region` | string | no | **(PREMIUM)** The AWS region the Elasticsearch domain is configured |
......
...@@ -1254,6 +1254,7 @@ module API ...@@ -1254,6 +1254,7 @@ module API
# let's not expose the secret key in a response # let's not expose the secret key in a response
attributes.delete(:asset_proxy_secret_key) attributes.delete(:asset_proxy_secret_key)
attributes.delete(:eks_secret_access_key)
attributes attributes
end end
......
...@@ -52,6 +52,12 @@ module API ...@@ -52,6 +52,12 @@ module API
optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups' optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups'
optional :domain_blacklist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' optional :domain_blacklist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
optional :domain_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' optional :domain_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
optional :eks_integration_enabled, type: Boolean, desc: 'Enable integration with Amazon EKS'
given eks_integration_enabled: -> (val) { val } do
requires :eks_account_id, type: String, desc: 'Amazon account ID for EKS integration'
requires :eks_access_key_id, type: String, desc: 'Access key ID for the EKS integration IAM user'
requires :eks_secret_access_key, type: String, desc: 'Secret access key for the EKS integration IAM user'
end
optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.' optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.' optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.' optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
......
...@@ -120,13 +120,22 @@ module Gitlab ...@@ -120,13 +120,22 @@ module Gitlab
@breakline_regex ||= /\r\n|\r|\n/ @breakline_regex ||= /\r\n|\r|\n/
end end
# https://docs.aws.amazon.com/general/latest/gr/acct-identifiers.html
def aws_account_id_regex
/\A\d{12}\z/
end
def aws_account_id_message
'must be a 12-digit number'
end
# https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html # https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
def aws_arn_regex def aws_arn_regex
/\Aarn:\S+\z/ /\Aarn:\S+\z/
end end
def aws_arn_regex_message def aws_arn_regex_message
"must be a valid Amazon Resource Name" 'must be a valid Amazon Resource Name'
end end
def utc_date_regex def utc_date_regex
......
...@@ -1480,6 +1480,15 @@ msgstr "" ...@@ -1480,6 +1480,15 @@ msgstr ""
msgid "Alternate support URL for help page and help dropdown" msgid "Alternate support URL for help page and help dropdown"
msgstr "" msgstr ""
msgid "Amazon EKS"
msgstr ""
msgid "Amazon EKS integration allows you to provision EKS clusters from GitLab."
msgstr ""
msgid "Amazon authentication is not %{link_start}property configured%{link_end}. Ask your GitLab administrator if you want to use this service."
msgstr ""
msgid "Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication" msgid "Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication"
msgstr "" msgstr ""
......
...@@ -318,6 +318,51 @@ describe Admin::ClustersController do ...@@ -318,6 +318,51 @@ describe Admin::ClustersController do
end end
end end
describe 'POST authorize AWS role for EKS cluster' do
let(:role_arn) { 'arn:aws:iam::123456789012:role/role-name' }
let(:role_external_id) { '12345' }
let(:params) do
{
cluster: {
role_arn: role_arn,
role_external_id: role_external_id
}
}
end
def go
post :authorize_aws_role, params: params
end
it 'creates an Aws::Role record' do
expect { go }.to change { Aws::Role.count }
expect(response.status).to eq 201
role = Aws::Role.last
expect(role.user).to eq admin
expect(role.role_arn).to eq role_arn
expect(role.role_external_id).to eq role_external_id
end
context 'role cannot be created' do
let(:role_arn) { 'invalid-role' }
it 'does not create a record' do
expect { go }.not_to change { Aws::Role.count }
expect(response.status).to eq 422
end
end
describe 'security' do
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
end
end
describe 'GET #cluster_status' do describe 'GET #cluster_status' do
let(:cluster) { create(:cluster, :providing_by_gcp, :instance) } let(:cluster) { create(:cluster, :providing_by_gcp, :instance) }
......
...@@ -372,6 +372,56 @@ describe Groups::ClustersController do ...@@ -372,6 +372,56 @@ describe Groups::ClustersController do
end end
end end
describe 'POST authorize AWS role for EKS cluster' do
let(:role_arn) { 'arn:aws:iam::123456789012:role/role-name' }
let(:role_external_id) { '12345' }
let(:params) do
{
cluster: {
role_arn: role_arn,
role_external_id: role_external_id
}
}
end
def go
post :authorize_aws_role, params: params.merge(group_id: group)
end
it 'creates an Aws::Role record' do
expect { go }.to change { Aws::Role.count }
expect(response.status).to eq 201
role = Aws::Role.last
expect(role.user).to eq user
expect(role.role_arn).to eq role_arn
expect(role.role_external_id).to eq role_external_id
end
context 'role cannot be created' do
let(:role_arn) { 'invalid-role' }
it 'does not create a record' do
expect { go }.not_to change { Aws::Role.count }
expect(response.status).to eq 422
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
describe 'GET cluster_status' do describe 'GET cluster_status' do
let(:cluster) { create(:cluster, :providing_by_gcp, cluster_type: :group_type, groups: [group]) } let(:cluster) { create(:cluster, :providing_by_gcp, cluster_type: :group_type, groups: [group]) }
......
...@@ -373,6 +373,56 @@ describe Projects::ClustersController do ...@@ -373,6 +373,56 @@ describe Projects::ClustersController do
end end
end end
describe 'POST authorize AWS role for EKS cluster' do
let(:role_arn) { 'arn:aws:iam::123456789012:role/role-name' }
let(:role_external_id) { '12345' }
let(:params) do
{
cluster: {
role_arn: role_arn,
role_external_id: role_external_id
}
}
end
def go
post :authorize_aws_role, params: params.merge(namespace_id: project.namespace, project_id: project)
end
it 'creates an Aws::Role record' do
expect { go }.to change { Aws::Role.count }
expect(response.status).to eq 201
role = Aws::Role.last
expect(role.user).to eq user
expect(role.role_arn).to eq role_arn
expect(role.role_external_id).to eq role_external_id
end
context 'role cannot be created' do
let(:role_arn) { 'invalid-role' }
it 'does not create a record' do
expect { go }.not_to change { Aws::Role.count }
expect(response.status).to eq 422
end
end
describe 'security' do
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:maintainer).of(project) }
it { expect { go }.to be_denied_for(:developer).of(project) }
it { expect { go }.to be_denied_for(:reporter).of(project) }
it { expect { go }.to be_denied_for(:guest).of(project) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
end
end
describe 'GET cluster_status' do describe 'GET cluster_status' do
let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) } let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
......
...@@ -13,7 +13,7 @@ describe 'Database schema' do ...@@ -13,7 +13,7 @@ describe 'Database schema' do
# EE: edit the ee/spec/db/schema_support.rb # EE: edit the ee/spec/db/schema_support.rb
IGNORED_FK_COLUMNS = { IGNORED_FK_COLUMNS = {
abuse_reports: %w[reporter_id user_id], abuse_reports: %w[reporter_id user_id],
application_settings: %w[performance_bar_allowed_group_id slack_app_id snowplow_site_id], application_settings: %w[performance_bar_allowed_group_id slack_app_id snowplow_site_id eks_account_id eks_access_key_id],
approvers: %w[target_id user_id], approvers: %w[target_id user_id],
approvals: %w[user_id], approvals: %w[user_id],
approver_groups: %w[target_id], approver_groups: %w[target_id],
......
...@@ -10,6 +10,7 @@ describe 'AWS EKS Cluster', :js do ...@@ -10,6 +10,7 @@ describe 'AWS EKS Cluster', :js do
project.add_maintainer(user) project.add_maintainer(user)
gitlab_sign_in(user) gitlab_sign_in(user)
allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 } allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
stub_application_setting(eks_integration_enabled: true)
end end
context 'when user does not have a cluster and visits cluster index page' do context 'when user does not have a cluster and visits cluster index page' do
......
...@@ -66,6 +66,15 @@ describe Gitlab::Regex do ...@@ -66,6 +66,15 @@ describe Gitlab::Regex do
end end
describe '.aws_account_id_regex' do describe '.aws_account_id_regex' do
subject { described_class.aws_account_id_regex }
it { is_expected.to match('123456789012') }
it { is_expected.not_to match('12345678901') }
it { is_expected.not_to match('1234567890123') }
it { is_expected.not_to match('12345678901a') }
end
describe '.aws_arn_regex' do
subject { described_class.aws_arn_regex } subject { described_class.aws_arn_regex }
it { is_expected.to match('arn:aws:iam::123456789012:role/role-name') } it { is_expected.to match('arn:aws:iam::123456789012:role/role-name') }
......
...@@ -106,6 +106,37 @@ describe ApplicationSetting do ...@@ -106,6 +106,37 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:lets_encrypt_notification_email) } it { is_expected.not_to allow_value(nil).for(:lets_encrypt_notification_email) }
end end
describe 'EKS integration' do
before do
setting.eks_integration_enabled = eks_enabled
end
context 'integration is disabled' do
let(:eks_enabled) { false }
it { is_expected.to allow_value(nil).for(:eks_account_id) }
it { is_expected.to allow_value(nil).for(:eks_access_key_id) }
it { is_expected.to allow_value(nil).for(:eks_secret_access_key) }
end
context 'integration is enabled' do
let(:eks_enabled) { true }
it { is_expected.to allow_value('123456789012').for(:eks_account_id) }
it { is_expected.not_to allow_value(nil).for(:eks_account_id) }
it { is_expected.not_to allow_value('123').for(:eks_account_id) }
it { is_expected.not_to allow_value('12345678901a').for(:eks_account_id) }
it { is_expected.to allow_value('access-key-id-12').for(:eks_access_key_id) }
it { is_expected.not_to allow_value('a' * 129).for(:eks_access_key_id) }
it { is_expected.not_to allow_value('short-key').for(:eks_access_key_id) }
it { is_expected.not_to allow_value(nil).for(:eks_access_key_id) }
it { is_expected.to allow_value('secret-access-key').for(:eks_secret_access_key) }
it { is_expected.not_to allow_value(nil).for(:eks_secret_access_key) }
end
end
describe 'default_artifacts_expire_in' do describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a') setting.update(default_artifacts_expire_in: 'a')
......
...@@ -31,4 +31,56 @@ describe Aws::Role do ...@@ -31,4 +31,56 @@ describe Aws::Role do
end end
end end
end end
describe 'callbacks' do
describe '#ensure_role_external_id' do
subject { role.validate }
context 'for a new record' do
let(:role) { build(:aws_role, role_external_id: nil) }
it 'calls #ensure_role_external_id!' do
expect(role).to receive(:ensure_role_external_id!)
subject
end
end
context 'for an existing record' do
let(:role) { create(:aws_role) }
it 'does not call #ensure_role_external_id!' do
expect(role).not_to receive(:ensure_role_external_id!)
subject
end
end
end
end
describe '#ensure_role_external_id!' do
let(:role) { build(:aws_role, role_external_id: external_id) }
subject { role.ensure_role_external_id! }
context 'role_external_id is blank' do
let(:external_id) { nil }
it 'generates an external ID and assigns it to the record' do
subject
expect(role.role_external_id).to be_present
end
end
context 'role_external_id is already set' do
let(:external_id) { 'external-id' }
it 'does not change the existing external id' do
subject
expect(role.role_external_id).to eq external_id
end
end
end
end end
...@@ -43,6 +43,12 @@ describe GroupClusterablePresenter do ...@@ -43,6 +43,12 @@ describe GroupClusterablePresenter do
it { is_expected.to eq(new_group_cluster_path(group)) } it { is_expected.to eq(new_group_cluster_path(group)) }
end end
describe '#authorize_aws_role_path' do
subject { presenter.authorize_aws_role_path }
it { is_expected.to eq(authorize_aws_role_group_clusters_path(group)) }
end
describe '#create_user_clusters_path' do describe '#create_user_clusters_path' do
subject { presenter.create_user_clusters_path } subject { presenter.create_user_clusters_path }
......
...@@ -43,6 +43,12 @@ describe ProjectClusterablePresenter do ...@@ -43,6 +43,12 @@ describe ProjectClusterablePresenter do
it { is_expected.to eq(new_project_cluster_path(project)) } it { is_expected.to eq(new_project_cluster_path(project)) }
end end
describe '#authorize_aws_role_path' do
subject { presenter.authorize_aws_role_path }
it { is_expected.to eq(authorize_aws_role_project_clusters_path(project)) }
end
describe '#create_user_clusters_path' do describe '#create_user_clusters_path' do
subject { presenter.create_user_clusters_path } subject { presenter.create_user_clusters_path }
......
...@@ -271,6 +271,61 @@ describe API::Settings, 'Settings' do ...@@ -271,6 +271,61 @@ describe API::Settings, 'Settings' do
end end
end end
context 'EKS integration settings' do
let(:attribute_names) { settings.keys.map(&:to_s) }
let(:sensitive_attributes) { %w(eks_secret_access_key) }
let(:exposed_attributes) { attribute_names - sensitive_attributes }
let(:settings) do
{
eks_integration_enabled: true,
eks_account_id: '123456789012',
eks_access_key_id: 'access-key-id-12',
eks_secret_access_key: 'secret-access-key'
}
end
it 'includes attributes in the API' do
get api("/application/settings", admin)
expect(response).to have_gitlab_http_status(200)
exposed_attributes.each do |attribute|
expect(json_response.keys).to include(attribute)
end
end
it 'does not include sensitive attributes in the API' do
get api("/application/settings", admin)
expect(response).to have_gitlab_http_status(200)
sensitive_attributes.each do |attribute|
expect(json_response.keys).not_to include(attribute)
end
end
it 'allows updating the settings' do
put api("/application/settings", admin), params: settings
expect(response).to have_gitlab_http_status(200)
settings.each do |attribute, value|
expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
end
end
context 'EKS integration is enabled but params are blank' do
let(:settings) { Hash[eks_integration_enabled: true] }
it 'does not update the settings' do
put api("/application/settings", admin), params: settings
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to include('eks_account_id is missing')
expect(json_response['error']).to include('eks_access_key_id is missing')
expect(json_response['error']).to include('eks_secret_access_key is missing')
end
end
end
context "missing plantuml_url value when plantuml_enabled is true" do context "missing plantuml_url value when plantuml_enabled is true" do
it "returns a blank parameter error message" do it "returns a blank parameter error message" do
put api("/application/settings", admin), params: { plantuml_enabled: true } put api("/application/settings", admin), params: { plantuml_enabled: true }
......
...@@ -13,15 +13,6 @@ describe Clusters::Aws::FetchCredentialsService do ...@@ -13,15 +13,6 @@ describe Clusters::Aws::FetchCredentialsService do
let(:sts_client) { Aws::STS::Client.new(credentials: gitlab_credentials, region: provider.region) } let(:sts_client) { Aws::STS::Client.new(credentials: gitlab_credentials, region: provider.region) }
let(:assumed_role) { instance_double(Aws::AssumeRoleCredentials, credentials: assumed_role_credentials) } let(:assumed_role) { instance_double(Aws::AssumeRoleCredentials, credentials: assumed_role_credentials) }
let(:kubernetes_provisioner_settings) do
{
aws: {
access_key_id: gitlab_access_key_id,
secret_access_key: gitlab_secret_access_key
}
}
end
let(:assumed_role_credentials) { double } let(:assumed_role_credentials) { double }
subject { described_class.new(provider).execute } subject { described_class.new(provider).execute }
...@@ -30,7 +21,8 @@ describe Clusters::Aws::FetchCredentialsService do ...@@ -30,7 +21,8 @@ describe Clusters::Aws::FetchCredentialsService do
let(:provision_role) { create(:aws_role, user: provider.created_by_user) } let(:provision_role) { create(:aws_role, user: provider.created_by_user) }
before do before do
stub_config(kubernetes: { provisioners: kubernetes_provisioner_settings }) stub_application_setting(eks_access_key_id: gitlab_access_key_id)
stub_application_setting(eks_secret_access_key: gitlab_secret_access_key)
expect(Aws::Credentials).to receive(:new) expect(Aws::Credentials).to receive(:new)
.with(gitlab_access_key_id, gitlab_secret_access_key) .with(gitlab_access_key_id, gitlab_secret_access_key)
......
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