Commit cfed1f7c authored by Thong Kuah's avatar Thong Kuah

Merge branch '22392-capture-aws-role-details' into 'master'

Capture AWS role for creating EKS clusters

See merge request gitlab-org/gitlab!18307
parents d6dc4d03 ef34cb6b
<script>
import { mapState } from 'vuex';
import ServiceCredentialsForm from './service_credentials_form.vue';
import EksClusterConfigurationForm from './eks_cluster_configuration_form.vue';
......@@ -16,14 +17,36 @@ export default {
type: String,
required: true,
},
accountAndExternalIdsHelpPath: {
type: String,
required: true,
},
createRoleArnHelpPath: {
type: String,
required: true,
},
externalLinkIcon: {
type: String,
required: true,
},
},
computed: {
...mapState(['hasCredentials']),
},
};
</script>
<template>
<div class="js-create-eks-cluster">
<eks-cluster-configuration-form
v-if="hasCredentials"
:gitlab-managed-cluster-help-path="gitlabManagedClusterHelpPath"
:kubernetes-integration-help-path="kubernetesIntegrationHelpPath"
/>
<service-credentials-form
v-else
:create-role-arn-help-path="createRoleArnHelpPath"
:account-and-external-ids-help-path="accountAndExternalIdsHelpPath"
:external-link-icon="externalLinkIcon"
/>
</div>
</template>
<script>
import { GlFormInput } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
export default {
components: {
GlFormInput,
LoadingButton,
ClipboardButton,
},
props: {
accountAndExternalIdsHelpPath: {
type: String,
required: true,
},
createRoleArnHelpPath: {
type: String,
required: true,
},
externalLinkIcon: {
type: String,
required: true,
},
},
data() {
return {
roleArn: '',
};
},
computed: {
...mapState(['accountId', 'externalId', 'isCreatingRole', 'createRoleError']),
submitButtonDisabled() {
return this.isCreatingRole || !this.roleArn;
},
submitButtonLabel() {
return this.isCreatingRole
? __('Authenticating')
: s__('ClusterIntegration|Authenticate with AWS');
},
accountAndExternalIdsHelpText() {
const escapedUrl = _.escape(this.accountAndExternalIdsHelpPath);
return sprintf(
s__(
'ClusterIntegration|Create a provision role on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the account and external ID above. %{startMoreInfoLink}More information%{endLink}',
),
{
startAwsLink:
'<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">',
startMoreInfoLink: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`,
externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
);
},
provisionRoleArnHelpText() {
const escapedUrl = _.escape(this.createRoleArnHelpPath);
return sprintf(
s__(
'ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provision role, first create one on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the above account and external IDs. %{startMoreInfoLink}More information%{endLink}',
),
{
startAwsLink:
'<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">',
startMoreInfoLink: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`,
externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
);
},
},
methods: {
...mapActions(['createRole']),
},
};
</script>
<template>
<form name="service-credentials-form"></form>
<form name="service-credentials-form" @submit.prevent="createRole({ roleArn, externalId })">
<h2>{{ s__('ClusterIntegration|Authenticate with Amazon Web Services') }}</h2>
<p>
{{
s__(
'ClusterIntegration|You must grant access to your organization’s AWS resources in order to create a new EKS cluster. To grant access, create a provision role using the account and external ID below and provide us the ARN.',
)
}}
</p>
<div v-if="createRoleError" class="js-invalid-credentials bs-callout bs-callout-danger">
{{ createRoleError }}
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="gitlab-account-id">{{ __('Account ID') }}</label>
<div class="input-group">
<gl-form-input id="gitlab-account-id" type="text" readonly :value="accountId" />
<div class="input-group-append">
<clipboard-button
:text="accountId"
:title="__('Copy Account ID to clipboard')"
class="input-group-text js-copy-account-id-button"
/>
</div>
</div>
</div>
<div class="form-group col-md-6">
<label for="eks-external-id">{{ __('External ID') }}</label>
<div class="input-group">
<gl-form-input id="eks-external-id" type="text" readonly :value="externalId" />
<div class="input-group-append">
<clipboard-button
:text="externalId"
:title="__('Copy External ID to clipboard')"
class="input-group-text js-copy-external-id-button"
/>
</div>
</div>
</div>
<div class="col-12 mb-3 mt-n3">
<p class="form-text text-muted" v-html="accountAndExternalIdsHelpText"></p>
</div>
</div>
<div class="form-group">
<label for="eks-provision-role-arn">{{ s__('ClusterIntegration|Provision Role ARN') }}</label>
<gl-form-input id="eks-provision-role-arn" v-model="roleArn" />
<p class="form-text text-muted" v-html="provisionRoleArnHelpText"></p>
</div>
<loading-button
class="js-submit-service-credentials"
type="submit"
:disabled="submitButtonDisabled"
:loading="isCreatingRole"
:label="submitButtonLabel"
/>
</form>
</template>
import Vue from 'vue';
import Vuex from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import CreateEksCluster from './components/create_eks_cluster.vue';
import createStore from './store';
Vue.use(Vuex);
export default el => {
const { gitlabManagedClusterHelpPath, kubernetesIntegrationHelpPath } = el.dataset;
const {
gitlabManagedClusterHelpPath,
kubernetesIntegrationHelpPath,
accountAndExternalIdsHelpPath,
createRoleArnHelpPath,
externalId,
accountId,
hasCredentials,
createRolePath,
externalLinkIcon,
} = el.dataset;
return new Vue({
el,
store: createStore(),
store: createStore({
initialState: {
hasCredentials: parseBoolean(hasCredentials),
externalId,
accountId,
createRolePath,
},
}),
components: {
CreateEksCluster,
},
......@@ -19,6 +37,9 @@ export default el => {
props: {
gitlabManagedClusterHelpPath,
kubernetesIntegrationHelpPath,
accountAndExternalIdsHelpPath,
createRoleArnHelpPath,
externalLinkIcon,
},
});
},
......
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
export const setClusterName = ({ commit }, payload) => {
commit(types.SET_CLUSTER_NAME, payload);
......@@ -12,6 +13,30 @@ export const setKubernetesVersion = ({ commit }, payload) => {
commit(types.SET_KUBERNETES_VERSION, payload);
};
export const createRole = ({ dispatch, state: { createRolePath } }, payload) => {
dispatch('requestCreateRole');
return axios
.post(createRolePath, {
role_arn: payload.roleArn,
role_external_id: payload.externalId,
})
.then(() => dispatch('createRoleSuccess'))
.catch(error => dispatch('createRoleError', { error }));
};
export const requestCreateRole = ({ commit }) => {
commit(types.REQUEST_CREATE_ROLE);
};
export const createRoleSuccess = ({ commit }) => {
commit(types.CREATE_ROLE_SUCCESS);
};
export const createRoleError = ({ commit }, payload) => {
commit(types.CREATE_ROLE_ERROR, payload);
};
export const setRegion = ({ commit }, payload) => {
commit(types.SET_REGION, payload);
};
......@@ -39,5 +64,3 @@ export const setSecurityGroup = ({ commit }, payload) => {
export const setGitlabManagedCluster = ({ commit }, payload) => {
commit(types.SET_GITLAB_MANAGED_CLUSTER, payload);
};
export default () => {};
......@@ -8,12 +8,12 @@ import clusterDropdownStore from './cluster_dropdown';
import * as awsServices from '../services/aws_services_facade';
const createStore = () =>
const createStore = ({ initialState }) =>
new Vuex.Store({
actions,
getters,
mutations,
state: state(),
state: Object.assign(state(), initialState),
modules: {
roles: {
namespaced: true,
......
......@@ -8,3 +8,6 @@ export const SET_SUBNET = 'SET_SUBNET';
export const SET_ROLE = 'SET_ROLE';
export const SET_SECURITY_GROUP = 'SET_SECURITY_GROUP';
export const SET_GITLAB_MANAGED_CLUSTER = 'SET_GITLAB_MANAGED_CLUSTER';
export const REQUEST_CREATE_ROLE = 'REQUEST_CREATE_ROLE';
export const CREATE_ROLE_SUCCESS = 'CREATE_ROLE_SUCCESS';
export const CREATE_ROLE_ERROR = 'CREATE_ROLE_ERROR';
......@@ -31,4 +31,19 @@ export default {
[types.SET_GITLAB_MANAGED_CLUSTER](state, { gitlabManagedCluster }) {
state.gitlabManagedCluster = gitlabManagedCluster;
},
[types.REQUEST_CREATE_ROLE](state) {
state.isCreatingRole = true;
state.createRoleError = null;
state.hasCredentials = false;
},
[types.CREATE_ROLE_SUCCESS](state) {
state.isCreatingRole = false;
state.createRoleError = null;
state.hasCredentials = true;
},
[types.CREATE_ROLE_ERROR](state, { error }) {
state.isCreatingRole = false;
state.createRoleError = error;
state.hasCredentials = false;
},
};
import { KUBERNETES_VERSIONS } from '../constants';
export default () => ({
isValidatingCredentials: false,
validCredentials: false,
createRolePath: null,
isCreatingRole: false,
roleCreated: false,
createRoleError: false,
accountId: '',
externalId: '',
clusterName: '',
environmentScope: '*',
......
......@@ -28,3 +28,6 @@
.border-color-blue-300 { border-color: $blue-300; }
.border-color-default { border-color: $border-color; }
.box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; }
.gl-w-64 { width: px-to-rem($grid-size * 8); }
.gl-h-64 { height: px-to-rem($grid-size * 8); }
......@@ -214,6 +214,10 @@ class ApplicationController < ActionController::Base
end
end
def respond_201
head :created
end
def respond_422
head :unprocessable_entity
end
......
......@@ -3,12 +3,12 @@
class Clusters::ClustersController < Clusters::BaseController
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 :validate_gcp_token, only: [:new]
before_action :gcp_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_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:cluster_status]
......@@ -43,10 +43,13 @@ class Clusters::ClustersController < Clusters::BaseController
def new
return unless Feature.enabled?(:create_eks_clusters)
@gke_selected = params[:provider] == 'gke'
@eks_selected = params[:provider] == 'eks'
if params[:provider] == 'aws'
@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] == 'gcp'
redirect_to @authorize_url if @authorize_url && !@valid_gcp_token
end
end
# Overridding ActionController::Metal#status is NOT a good idea
......@@ -132,6 +135,12 @@ class Clusters::ClustersController < Clusters::BaseController
end
end
def authorize_aws_role
role = current_user.build_aws_role(create_role_params)
role.save ? respond_201 : respond_422
end
private
def update_params
......@@ -203,6 +212,10 @@ class Clusters::ClustersController < Clusters::BaseController
)
end
def create_role_params
params.require(:cluster).permit(:role_arn, :role_external_id)
end
def generate_gcp_authorize_url
params = Feature.enabled?(:create_eks_clusters) ? { provider: :gke } : {}
state = generate_session_key_redirect(clusterable.new_path(params).to_s)
......
......@@ -193,6 +193,10 @@ module ApplicationSettingsHelper
:dsa_key_restriction,
:ecdsa_key_restriction,
:ed25519_key_restriction,
:eks_integration_enabled,
:eks_account_id,
:eks_access_key_id,
:eks_secret_access_key,
:email_author_in_body,
:enabled_git_access_protocol,
:enforce_terms,
......
......@@ -6,6 +6,28 @@ module ClustersHelper
false
end
def create_new_cluster_label(provider: nil)
case provider
when 'aws'
s_('ClusterIntegration|Create new Cluster on EKS')
when 'gcp'
s_('ClusterIntegration|Create new Cluster on GKE')
else
s_('ClusterIntegration|Create new Cluster')
end
end
def new_cluster_partial(provider: nil)
case provider
when 'aws'
'clusters/clusters/aws/new'
when 'gcp'
'clusters/clusters/gcp/new'
else
'clusters/clusters/cloud_providers/cloud_provider_selector'
end
end
def render_gcp_signup_offer
return if Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers?
return unless show_gcp_signup_offer?
......
......@@ -274,6 +274,22 @@ class ApplicationSetting < ApplicationRecord
presence: true,
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: :eks_integration_enabled?
validates :eks_access_key_id,
length: { in: 16..128 },
if: :eks_integration_enabled?
validates :eks_secret_access_key,
presence: true,
if: :eks_integration_enabled?
validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert,
pkey: :external_auth_client_key,
......@@ -304,6 +320,12 @@ class ApplicationSetting < ApplicationRecord
algorithm: 'aes-256-gcm',
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_save :ensure_runners_registration_token
......
......@@ -54,6 +54,10 @@ module ApplicationSettingImplementation
dsa_key_restriction: 0,
ecdsa_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,
gitaly_timeout_default: 55,
gitaly_timeout_fast: 10,
......
......@@ -13,5 +13,11 @@ module Aws
with: Gitlab::Regex.aws_arn_regex,
message: Gitlab::Regex.aws_arn_regex_message
}
before_validation :ensure_role_external_id!, on: :create
def ensure_role_external_id!
self.role_external_id ||= SecureRandom.hex(20)
end
end
end
......@@ -29,6 +29,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
new_polymorphic_path([clusterable, :cluster], options)
end
def authorize_aws_role_path
polymorphic_path([clusterable, :clusters], action: :authorize_aws_role)
end
def create_user_clusters_path
polymorphic_path([clusterable, :clusters], action: :create_user)
end
......
......@@ -52,6 +52,11 @@ class InstanceClusterablePresenter < ClusterablePresenter
create_gcp_admin_clusters_path
end
override :authorize_aws_role_path
def authorize_aws_role_path
authorize_aws_role_admin_clusters_path
end
override :empty_state_help_text
def empty_state_help_text
s_('ClusterIntegration|Adding an integration will share the cluster across all projects.')
......
......@@ -36,20 +36,12 @@ module Clusters
::Aws::Credentials.new(access_key_id, secret_access_key)
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
Gitlab.config.kubernetes.provisioners.aws.access_key_id
Gitlab::CurrentSettings.eks_access_key_id
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
Gitlab.config.kubernetes.provisioners.aws.secret_access_key
Gitlab::CurrentSettings.eks_secret_access_key
end
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 @@
= render 'admin/application_settings/third_party_offers'
= render 'admin/application_settings/snowplow'
= render_if_exists 'admin/application_settings/pendo'
= render 'admin/application_settings/eks' if Feature.enabled?(:create_eks_clusters)
- if !Gitlab::CurrentSettings.eks_integration_enabled?
- 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}correctly 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'),
'external-link-icon' => icon('external-link'),
'has-credentials' => @aws_role.role_arn.present?.to_s } }
- provider = local_assigns.fetch(:provider)
- logo_path = local_assigns.fetch(:logo_path)
- label = local_assigns.fetch(:label)
- last = local_assigns.fetch(:last, false)
- classes = ['btn btn-light btn-outline flex-fill d-inline-flex flex-column justify-content-center align-items-center', ('mr-3' unless last)]
= link_to clusterable.new_path(provider: provider), class: 'btn gl-button btn-outline flex-fill d-inline-flex flex-column mr-3 justify-content-center align-items-center' do
.svg-content= image_tag logo_path, alt: label, class: 'gl-w-13 gl-h-13'
= link_to clusterable.new_path(provider: provider), class: classes do
.svg-content.p-2= image_tag logo_path, alt: label, class: 'gl-w-64 gl-h-64'
%span
= label
......@@ -2,10 +2,10 @@
- eks_label = s_('ClusterIntegration|Amazon EKS')
- create_cluster_label = s_('ClusterIntegration|Create cluster on')
.d-flex.flex-column
%h5
%h5.mb-3
= create_cluster_label
.d-flex
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
locals: { provider: 'eks', label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg' }
locals: { provider: 'aws', label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg' }
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
locals: { provider: 'gke', label: gke_label, logo_path: 'illustrations/logos/google_gke.svg' }
locals: { provider: 'gcp', label: gke_label, logo_path: 'illustrations/logos/google_gke.svg', last: true }
.js-create-eks-cluster-form-container{ data: { 'gitlab-managed-cluster-help-path' => help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'),
'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index') } }
= render 'clusters/clusters/gcp/header'
- if @valid_gcp_token
= render 'clusters/clusters/gcp/form'
- elsif @authorize_url
= render 'clusters/clusters/gcp/signin_with_google_button'
- else
= render 'clusters/clusters/gcp/gcp_not_configured'
......@@ -2,9 +2,6 @@
- page_title _('Kubernetes Cluster')
- create_eks_enabled = Feature.enabled?(:create_eks_clusters)
- active_tab = local_assigns.fetch(:active_tab, 'create')
- create_on_gke_tab_label = s_('ClusterIntegration|Create new Cluster on GKE')
- create_on_eks_tab_label = s_('ClusterIntegration|Create new Cluster on EKS')
- create_new_cluster_label = s_('ClusterIntegration|Create new Cluster')
= javascript_include_tag 'https://apis.google.com/js/api.js'
= render_gcp_signup_offer
......@@ -18,14 +15,9 @@
%a.nav-link{ href: '#create-cluster-pane', id: 'create-cluster-tab', class: active_when(active_tab == 'create'), data: { toggle: 'tab' }, role: 'tab' }
%span
- if create_eks_enabled
- if @gke_selected
= create_on_gke_tab_label
- elsif @eks_selected
= create_on_eks_tab_label
- else
= create_new_cluster_label
= create_new_cluster_label(provider: params[:provider])
- else
= create_on_gke_tab_label
= create_new_cluster_label(provider: 'gcp')
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#add-cluster-pane', id: 'add-cluster-tab', class: active_when(active_tab == 'add'), data: { toggle: 'tab' }, role: 'tab' }
%span Add existing cluster
......@@ -33,27 +25,10 @@
.tab-content.gitlab-tab-content
- if create_eks_enabled
.tab-pane{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
- if @gke_selected
= render 'clusters/clusters/gcp/header'
- if @valid_gcp_token
= render 'clusters/clusters/gcp/form'
- elsif @authorize_url
= render 'clusters/clusters/gcp/signin_with_google_button'
- else
= render 'clusters/clusters/gcp/gcp_not_configured'
- elsif @eks_selected
= render 'clusters/clusters/eks/index'
- else
= render 'clusters/clusters/cloud_providers/cloud_provider_selector'
= render new_cluster_partial(provider: params[:provider])
- else
.tab-pane{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
= render 'clusters/clusters/gcp/header'
- if @valid_gcp_token
= render 'clusters/clusters/gcp/form'
- elsif @authorize_url
= render 'clusters/clusters/gcp/signin_with_google_button'
- else
= render 'clusters/clusters/gcp/gcp_not_configured'
= render new_cluster_partial(provider: 'gcp')
.tab-pane{ id: 'add-cluster-pane', class: active_when(active_tab == 'add'), role: 'tabpanel' }
= render 'clusters/clusters/user/header'
......
---
title: Add ApplicationSetting entries for EKS integration
merge_request: 18307
author:
type: other
......@@ -142,6 +142,7 @@ Rails.application.routes.draw do
collection do
post :create_user
post :create_gcp
post :authorize_aws_role
end
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
t.boolean "pendo_enabled", default: false, null: false
t.string "pendo_url", limit: 255
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 ["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"
......
......@@ -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. |
| `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. |
| `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` | boolean | no | **(PREMIUM)** Enable the use of AWS hosted Elasticsearch |
| `elasticsearch_aws_region` | string | no | **(PREMIUM)** The AWS region the Elasticsearch domain is configured |
......
......@@ -1254,6 +1254,7 @@ module API
# let's not expose the secret key in a response
attributes.delete(:asset_proxy_secret_key)
attributes.delete(:eks_secret_access_key)
attributes
end
......
......@@ -52,6 +52,12 @@ module API
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_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 :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.'
......
......@@ -120,13 +120,22 @@ module Gitlab
@breakline_regex ||= /\r\n|\r|\n/
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
def aws_arn_regex
/\Aarn:\S+\z/
end
def aws_arn_regex_message
"must be a valid Amazon Resource Name"
'must be a valid Amazon Resource Name'
end
def utc_date_regex
......
......@@ -849,6 +849,9 @@ msgstr ""
msgid "Account"
msgstr ""
msgid "Account ID"
msgstr ""
msgid "Account and limit"
msgstr ""
......@@ -1483,6 +1486,15 @@ msgstr ""
msgid "Alternate support URL for help page and help dropdown"
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}correctly 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"
msgstr ""
......@@ -2164,6 +2176,9 @@ msgstr ""
msgid "Authenticate with GitHub"
msgstr ""
msgid "Authenticating"
msgstr ""
msgid "Authentication Log"
msgstr ""
......@@ -3502,6 +3517,12 @@ msgstr ""
msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster."
msgstr ""
msgid "ClusterIntegration|Authenticate with AWS"
msgstr ""
msgid "ClusterIntegration|Authenticate with Amazon Web Services"
msgstr ""
msgid "ClusterIntegration|Base domain"
msgstr ""
......@@ -3592,6 +3613,9 @@ msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Create a provision role on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the account and external ID above. %{startMoreInfoLink}More information%{endLink}"
msgstr ""
msgid "ClusterIntegration|Create cluster on"
msgstr ""
......@@ -3889,6 +3913,9 @@ msgstr ""
msgid "ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications."
msgstr ""
msgid "ClusterIntegration|Provision Role ARN"
msgstr ""
msgid "ClusterIntegration|RBAC-enabled cluster"
msgstr ""
......@@ -4024,6 +4051,9 @@ msgstr ""
msgid "ClusterIntegration|Subnet"
msgstr ""
msgid "ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provision role, first create one on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the above account and external IDs. %{startMoreInfoLink}More information%{endLink}"
msgstr ""
msgid "ClusterIntegration|The Kubernetes certificate used to authenticate to the cluster."
msgstr ""
......@@ -4093,6 +4123,9 @@ msgstr ""
msgid "ClusterIntegration|You must first install Helm Tiller before installing the applications below"
msgstr ""
msgid "ClusterIntegration|You must grant access to your organization’s AWS resources in order to create a new EKS cluster. To grant access, create a provision role using the account and external ID below and provide us the ARN."
msgstr ""
msgid "ClusterIntegration|You must have an RBAC-enabled cluster to install Knative."
msgstr ""
......@@ -4592,6 +4625,12 @@ msgstr ""
msgid "Copy %{proxy_url}"
msgstr ""
msgid "Copy Account ID to clipboard"
msgstr ""
msgid "Copy External ID to clipboard"
msgstr ""
msgid "Copy ID"
msgstr ""
......@@ -6868,6 +6907,9 @@ msgstr ""
msgid "External Classification Policy Authorization"
msgstr ""
msgid "External ID"
msgstr ""
msgid "External URL"
msgstr ""
......
......@@ -73,7 +73,7 @@ describe Admin::ClustersController do
end
describe 'GET #new' do
def get_new(provider: 'gke')
def get_new(provider: 'gcp')
get :new, params: { provider: provider }
end
......@@ -318,6 +318,51 @@ describe Admin::ClustersController do
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
let(:cluster) { create(:cluster, :providing_by_gcp, :instance) }
......
......@@ -85,7 +85,7 @@ describe Groups::ClustersController do
end
describe 'GET new' do
def go(provider: 'gke')
def go(provider: 'gcp')
get :new, params: { group_id: group, provider: provider }
end
......@@ -372,6 +372,56 @@ describe Groups::ClustersController do
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
let(:cluster) { create(:cluster, :providing_by_gcp, cluster_type: :group_type, groups: [group]) }
......
......@@ -79,7 +79,7 @@ describe Projects::ClustersController do
end
describe 'GET new' do
def go(provider: 'gke')
def go(provider: 'gcp')
get :new, params: {
namespace_id: project.namespace,
project_id: project,
......@@ -373,6 +373,56 @@ describe Projects::ClustersController do
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
let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
......
......@@ -13,7 +13,7 @@ describe 'Database schema' do
# EE: edit the ee/spec/db/schema_support.rb
IGNORED_FK_COLUMNS = {
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],
approvals: %w[user_id],
approver_groups: %w[target_id],
......
......@@ -10,6 +10,7 @@ describe 'AWS EKS Cluster', :js do
project.add_maintainer(user)
gitlab_sign_in(user)
allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
stub_application_setting(eks_integration_enabled: true)
end
context 'when user does not have a cluster and visits cluster index page' do
......
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import CreateEksCluster from '~/create_cluster/eks_cluster/components/create_eks_cluster.vue';
import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue';
import ServiceCredentialsForm from '~/create_cluster/eks_cluster/components/service_credentials_form.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('CreateEksCluster', () => {
let vm;
let state;
const gitlabManagedClusterHelpPath = 'gitlab-managed-cluster-help-path';
const accountAndExternalIdsHelpPath = 'account-and-external-id-help-path';
const createRoleArnHelpPath = 'role-arn-help-path';
const kubernetesIntegrationHelpPath = 'kubernetes-integration';
const externalLinkIcon = 'external-link';
beforeEach(() => {
state = { hasCredentials: false };
const store = new Vuex.Store({
state,
});
vm = shallowMount(CreateEksCluster, {
propsData: {
gitlabManagedClusterHelpPath,
accountAndExternalIdsHelpPath,
createRoleArnHelpPath,
externalLinkIcon,
kubernetesIntegrationHelpPath,
},
localVue,
store,
});
});
afterEach(() => vm.destroy());
describe('when credentials are provided', () => {
beforeEach(() => {
state.hasCredentials = true;
});
it('displays eks cluster configuration form when credentials are valid', () => {
expect(vm.find(EksClusterConfigurationForm).exists()).toBe(true);
});
describe('passes to the cluster configuration form', () => {
it('help url for kubernetes integration documentation', () => {
expect(vm.find(EksClusterConfigurationForm).props('gitlabManagedClusterHelpPath')).toBe(
gitlabManagedClusterHelpPath,
);
});
it('help url for gitlab managed cluster documentation', () => {
expect(vm.find(EksClusterConfigurationForm).props('kubernetesIntegrationHelpPath')).toBe(
kubernetesIntegrationHelpPath,
);
});
});
});
describe('when credentials are invalid', () => {
beforeEach(() => {
state.hasCredentials = false;
});
it('displays service credentials form', () => {
expect(vm.find(ServiceCredentialsForm).exists()).toBe(true);
});
describe('passes to the service credentials form', () => {
it('help url for account and external ids', () => {
expect(vm.find(ServiceCredentialsForm).props('accountAndExternalIdsHelpPath')).toBe(
accountAndExternalIdsHelpPath,
);
});
it('external link icon', () => {
expect(vm.find(ServiceCredentialsForm).props('externalLinkIcon')).toBe(externalLinkIcon);
});
it('help url to create a role ARN', () => {
expect(vm.find(ServiceCredentialsForm).props('createRoleArnHelpPath')).toBe(
createRoleArnHelpPath,
);
});
});
});
});
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import ServiceCredentialsForm from '~/create_cluster/eks_cluster/components/service_credentials_form.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eksClusterState from '~/create_cluster/eks_cluster/store/state';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ServiceCredentialsForm', () => {
let vm;
let state;
let createRoleAction;
const accountId = 'accountId';
const externalId = 'externalId';
beforeEach(() => {
state = Object.assign(eksClusterState(), {
accountId,
externalId,
});
createRoleAction = jest.fn();
const store = new Vuex.Store({
state,
actions: {
createRole: createRoleAction,
},
});
vm = shallowMount(ServiceCredentialsForm, {
propsData: {
accountAndExternalIdsHelpPath: '',
createRoleArnHelpPath: '',
externalLinkIcon: '',
},
localVue,
store,
});
});
afterEach(() => vm.destroy());
const findAccountIdInput = () => vm.find('#gitlab-account-id');
const findCopyAccountIdButton = () => vm.find('.js-copy-account-id-button');
const findExternalIdInput = () => vm.find('#eks-external-id');
const findCopyExternalIdButton = () => vm.find('.js-copy-external-id-button');
const findInvalidCredentials = () => vm.find('.js-invalid-credentials');
const findSubmitButton = () => vm.find(LoadingButton);
const findForm = () => vm.find('form[name="service-credentials-form"]');
it('displays provided account id', () => {
expect(findAccountIdInput().attributes('value')).toBe(accountId);
});
it('allows to copy account id', () => {
expect(findCopyAccountIdButton().props('text')).toBe(accountId);
});
it('displays provided external id', () => {
expect(findExternalIdInput().attributes('value')).toBe(externalId);
});
it('allows to copy external id', () => {
expect(findCopyExternalIdButton().props('text')).toBe(externalId);
});
it('disables submit button when role ARN is not provided', () => {
expect(findSubmitButton().attributes('disabled')).toBeTruthy();
});
it('enables submit button when role ARN is not provided', () => {
vm.setData({ roleArn: '123' });
expect(findSubmitButton().attributes('disabled')).toBeFalsy();
});
it('dispatches createRole action when form is submitted', () => {
findForm().trigger('submit');
expect(createRoleAction).toHaveBeenCalled();
});
describe('when is creating role', () => {
beforeEach(() => {
vm.setData({ roleArn: '123' }); // set role ARN to enable button
state.isCreatingRole = true;
});
it('disables submit button', () => {
expect(findSubmitButton().props('disabled')).toBe(true);
});
it('sets submit button as loading', () => {
expect(findSubmitButton().props('loading')).toBe(true);
});
it('displays Authenticating label on submit button', () => {
expect(findSubmitButton().props('label')).toBe('Authenticating');
});
});
describe('when role can’t be created', () => {
beforeEach(() => {
state.createRoleError = 'Invalid credentials';
});
it('displays invalid role warning banner', () => {
expect(findInvalidCredentials().exists()).toBe(true);
});
it('displays invalid role error message', () => {
expect(findInvalidCredentials().text()).toContain(state.createRoleError);
});
});
});
......@@ -13,7 +13,12 @@ import {
SET_ROLE,
SET_SECURITY_GROUP,
SET_GITLAB_MANAGED_CLUSTER,
REQUEST_CREATE_ROLE,
CREATE_ROLE_SUCCESS,
CREATE_ROLE_ERROR,
} from '~/create_cluster/eks_cluster/store/mutation_types';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
describe('EKS Cluster Store Actions', () => {
let clusterName;
......@@ -26,6 +31,8 @@ describe('EKS Cluster Store Actions', () => {
let keyPair;
let securityGroup;
let gitlabManagedCluster;
let mock;
let state;
beforeEach(() => {
clusterName = 'my cluster';
......@@ -38,6 +45,19 @@ describe('EKS Cluster Store Actions', () => {
keyPair = { name: 'key-pair-1' };
securityGroup = { name: 'default group' };
gitlabManagedCluster = true;
state = {
...createState(),
createRolePath: '/clusters/roles/',
};
});
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
it.each`
......@@ -55,6 +75,78 @@ describe('EKS Cluster Store Actions', () => {
`(`$action commits $mutation with $payloadDescription payload`, data => {
const { action, mutation, payload } = data;
testAction(actions[action], payload, createState(), [{ type: mutation, payload }]);
testAction(actions[action], payload, state, [{ type: mutation, payload }]);
});
describe('createRole', () => {
const payload = {
roleArn: 'role_arn',
externalId: 'externalId',
};
describe('when request succeeds', () => {
beforeEach(() => {
mock
.onPost(state.createRolePath, {
role_arn: payload.roleArn,
role_external_id: payload.externalId,
})
.reply(201);
});
it('dispatches createRoleSuccess action', () =>
testAction(
actions.createRole,
payload,
state,
[],
[{ type: 'requestCreateRole' }, { type: 'createRoleSuccess' }],
));
});
describe('when request fails', () => {
let error;
beforeEach(() => {
error = new Error('Request failed with status code 400');
mock
.onPost(state.createRolePath, {
role_arn: payload.roleArn,
role_external_id: payload.externalId,
})
.reply(400, error);
});
it('dispatches createRoleError action', () =>
testAction(
actions.createRole,
payload,
state,
[],
[{ type: 'requestCreateRole' }, { type: 'createRoleError', payload: { error } }],
));
});
});
describe('requestCreateRole', () => {
it('commits requestCreaterole mutation', () => {
testAction(actions.requestCreateRole, null, state, [{ type: REQUEST_CREATE_ROLE }]);
});
});
describe('createRoleSuccess', () => {
it('commits createRoleSuccess mutation', () => {
testAction(actions.createRoleSuccess, null, state, [{ type: CREATE_ROLE_SUCCESS }]);
});
});
describe('createRoleError', () => {
it('commits createRoleError mutation', () => {
const payload = {
error: new Error(),
};
testAction(actions.createRoleError, payload, state, [{ type: CREATE_ROLE_ERROR, payload }]);
});
});
});
......@@ -9,6 +9,9 @@ import {
SET_ROLE,
SET_SECURITY_GROUP,
SET_GITLAB_MANAGED_CLUSTER,
REQUEST_CREATE_ROLE,
CREATE_ROLE_SUCCESS,
CREATE_ROLE_ERROR,
} from '~/create_cluster/eks_cluster/store/mutation_types';
import createState from '~/create_cluster/eks_cluster/store/state';
import mutations from '~/create_cluster/eks_cluster/store/mutations';
......@@ -59,4 +62,60 @@ describe('Create EKS cluster store mutations', () => {
mutations[mutation](state, payload);
expect(state[mutatedProperty]).toBe(expectedValue);
});
describe(`mutation ${REQUEST_CREATE_ROLE}`, () => {
beforeEach(() => {
mutations[REQUEST_CREATE_ROLE](state);
});
it('sets isCreatingRole to true', () => {
expect(state.isCreatingRole).toBe(true);
});
it('sets createRoleError to null', () => {
expect(state.createRoleError).toBe(null);
});
it('sets hasCredentials to false', () => {
expect(state.hasCredentials).toBe(false);
});
});
describe(`mutation ${CREATE_ROLE_SUCCESS}`, () => {
beforeEach(() => {
mutations[CREATE_ROLE_SUCCESS](state);
});
it('sets isCreatingRole to false', () => {
expect(state.isCreatingRole).toBe(false);
});
it('sets createRoleError to null', () => {
expect(state.createRoleError).toBe(null);
});
it('sets hasCredentials to false', () => {
expect(state.hasCredentials).toBe(true);
});
});
describe(`mutation ${CREATE_ROLE_ERROR}`, () => {
const error = new Error();
beforeEach(() => {
mutations[CREATE_ROLE_ERROR](state, { error });
});
it('sets isCreatingRole to false', () => {
expect(state.isCreatingRole).toBe(false);
});
it('sets createRoleError to the error object', () => {
expect(state.createRoleError).toBe(error);
});
it('sets hasCredentials to false', () => {
expect(state.hasCredentials).toBe(false);
});
});
});
......@@ -30,4 +30,60 @@ describe ClustersHelper do
end
end
end
describe '#create_new_cluster_label' do
subject { helper.create_new_cluster_label(provider: provider) }
context 'GCP provider' do
let(:provider) { 'gcp' }
it { is_expected.to eq('Create new Cluster on GKE') }
end
context 'AWS provider' do
let(:provider) { 'aws' }
it { is_expected.to eq('Create new Cluster on EKS') }
end
context 'other provider' do
let(:provider) { 'other' }
it { is_expected.to eq('Create new Cluster') }
end
context 'no provider' do
let(:provider) { nil }
it { is_expected.to eq('Create new Cluster') }
end
end
describe '#render_new_provider_form' do
subject { helper.new_cluster_partial(provider: provider) }
context 'GCP provider' do
let(:provider) { 'gcp' }
it { is_expected.to eq('clusters/clusters/gcp/new') }
end
context 'AWS provider' do
let(:provider) { 'aws' }
it { is_expected.to eq('clusters/clusters/aws/new') }
end
context 'other provider' do
let(:provider) { 'other' }
it { is_expected.to eq('clusters/clusters/cloud_providers/cloud_provider_selector') }
end
context 'no provider' do
let(:provider) { nil }
it { is_expected.to eq('clusters/clusters/cloud_providers/cloud_provider_selector') }
end
end
end
......@@ -66,6 +66,15 @@ describe Gitlab::Regex do
end
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 }
it { is_expected.to match('arn:aws:iam::123456789012:role/role-name') }
......
......@@ -106,6 +106,37 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:lets_encrypt_notification_email) }
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
it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a')
......
......@@ -31,4 +31,56 @@ describe Aws::Role do
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
......@@ -43,6 +43,12 @@ describe GroupClusterablePresenter do
it { is_expected.to eq(new_group_cluster_path(group)) }
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
subject { presenter.create_user_clusters_path }
......
......@@ -43,6 +43,12 @@ describe ProjectClusterablePresenter do
it { is_expected.to eq(new_project_cluster_path(project)) }
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
subject { presenter.create_user_clusters_path }
......
......@@ -271,6 +271,61 @@ describe API::Settings, 'Settings' do
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
it "returns a blank parameter error message" do
put api("/application/settings", admin), params: { plantuml_enabled: true }
......
......@@ -13,15 +13,6 @@ describe Clusters::Aws::FetchCredentialsService do
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(: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 }
subject { described_class.new(provider).execute }
......@@ -30,7 +21,8 @@ describe Clusters::Aws::FetchCredentialsService do
let(:provision_role) { create(:aws_role, user: provider.created_by_user) }
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)
.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