diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-08 09:06:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-08 09:06:24 +0300 |
commit | 1ef4b65f55f4fc6524a47050b4f6d686beb81d3a (patch) | |
tree | 3efc2710e564b86e5e2420d65457f656454006bb | |
parent | 18a102a5b95198b6bc8db2589de6353997a33543 (diff) |
Add latest changes from gitlab-org/gitlab@master
58 files changed, 1371 insertions, 115 deletions
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue index 22ee368b8e0..6bcae6ab536 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue @@ -1,4 +1,5 @@ <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> diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue index 79029b8cfa8..185fecba2d8 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue @@ -1,3 +1,141 @@ +<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> diff --git a/app/assets/javascripts/create_cluster/eks_cluster/index.js b/app/assets/javascripts/create_cluster/eks_cluster/index.js index 1f595e9b2df..e634a743d1d 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/index.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/index.js @@ -1,16 +1,34 @@ 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, }, }); }, diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js index 917c8da6c3e..16a7547957e 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js @@ -1,4 +1,5 @@ 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 () => {}; diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js index d575deafd19..22cca5b816e 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js @@ -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, diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js b/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js index 82eb512ac07..398b48d725f 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js @@ -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'; diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js b/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js index 79950ac7dce..f7752a23574 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js @@ -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; + }, }; diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js index bf74213bdce..b69ae5b51e5 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js @@ -1,8 +1,14 @@ import { KUBERNETES_VERSIONS } from '../constants'; export default () => ({ - isValidatingCredentials: false, - validCredentials: false, + createRolePath: null, + + isCreatingRole: false, + roleCreated: false, + createRoleError: false, + + accountId: '', + externalId: '', clusterName: '', environmentScope: '*', diff --git a/app/assets/javascripts/vue_shared/components/slot_switch.vue b/app/assets/javascripts/vue_shared/components/slot_switch.vue new file mode 100644 index 00000000000..67726f01744 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/slot_switch.vue @@ -0,0 +1,35 @@ +<script> +/** + * Allows to toggle slots based on an array of slot names. + */ +export default { + name: 'SlotSwitch', + + props: { + activeSlotNames: { + type: Array, + required: true, + }, + + tagName: { + type: String, + required: false, + default: 'div', + }, + }, + + computed: { + allSlotNames() { + return Object.keys(this.$slots); + }, + }, +}; +</script> + +<template> + <component :is="tagName"> + <template v-for="slotName in allSlotNames"> + <slot v-if="activeSlotNames.includes(slotName)" :name="slotName"></slot> + </template> + </component> +</template> diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index f53b6fbb1e7..3b3a2778b23 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -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); } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 30882f5a9f7..7329753ac54 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -214,6 +214,10 @@ class ApplicationController < ActionController::Base end end + def respond_201 + head :created + end + def respond_422 head :unprocessable_entity end diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index abec237dd1d..7c5c4bb8e80 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -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) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 02226cc1651..ec2e1648904 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -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, diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 7ca509873cc..0cfb45a12e5 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -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? diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 07335b6a883..6a34f293a4a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -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 diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 6cc77cca8a3..9119f8766eb 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -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, diff --git a/app/models/aws/role.rb b/app/models/aws/role.rb index 836107435ad..54132be749d 100644 --- a/app/models/aws/role.rb +++ b/app/models/aws/role.rb @@ -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 diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb index 34dffbf40fd..d6f67c1f2e5 100644 --- a/app/presenters/clusterable_presenter.rb +++ b/app/presenters/clusterable_presenter.rb @@ -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 diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb index 908cd17678d..f820c0f6b42 100644 --- a/app/presenters/instance_clusterable_presenter.rb +++ b/app/presenters/instance_clusterable_presenter.rb @@ -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.') diff --git a/app/services/clusters/aws/fetch_credentials_service.rb b/app/services/clusters/aws/fetch_credentials_service.rb index e1d04fac976..29442208c62 100644 --- a/app/services/clusters/aws/fetch_credentials_service.rb +++ b/app/services/clusters/aws/fetch_credentials_service.rb @@ -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 diff --git a/app/views/admin/application_settings/_eks.html.haml b/app/views/admin/application_settings/_eks.html.haml new file mode 100644 index 00000000000..b1f7ed76281 --- /dev/null +++ b/app/views/admin/application_settings/_eks.html.haml @@ -0,0 +1,31 @@ +- 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" diff --git a/app/views/admin/application_settings/integrations.html.haml b/app/views/admin/application_settings/integrations.html.haml index 0045a149c97..519d2bf9bbc 100644 --- a/app/views/admin/application_settings/integrations.html.haml +++ b/app/views/admin/application_settings/integrations.html.haml @@ -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) diff --git a/app/views/clusters/clusters/aws/_new.html.haml b/app/views/clusters/clusters/aws/_new.html.haml new file mode 100644 index 00000000000..fe8b606af70 --- /dev/null +++ b/app/views/clusters/clusters/aws/_new.html.haml @@ -0,0 +1,11 @@ +- 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 } } diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml index d4999798c19..56d46580b9e 100644 --- a/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml +++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml @@ -1,8 +1,10 @@ - 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 diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml index 7a93a7604f5..91925f5f96f 100644 --- a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml +++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml @@ -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 } diff --git a/app/views/clusters/clusters/eks/_index.html.haml b/app/views/clusters/clusters/eks/_index.html.haml deleted file mode 100644 index db64698a7f2..00000000000 --- a/app/views/clusters/clusters/eks/_index.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -.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') } } diff --git a/app/views/clusters/clusters/gcp/_new.html.haml b/app/views/clusters/clusters/gcp/_new.html.haml new file mode 100644 index 00000000000..3d47f4bf2c3 --- /dev/null +++ b/app/views/clusters/clusters/gcp/_new.html.haml @@ -0,0 +1,7 @@ += 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' diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml index 2c23426aaf9..cb8cbe4e6f2 100644 --- a/app/views/clusters/clusters/new.html.haml +++ b/app/views/clusters/clusters/new.html.haml @@ -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' diff --git a/changelogs/unreleased/22392-capture-aws-role-details.yml b/changelogs/unreleased/22392-capture-aws-role-details.yml new file mode 100644 index 00000000000..51b70d94f6a --- /dev/null +++ b/changelogs/unreleased/22392-capture-aws-role-details.yml @@ -0,0 +1,5 @@ +--- +title: Add ApplicationSetting entries for EKS integration +merge_request: 18307 +author: +type: other diff --git a/config/routes.rb b/config/routes.rb index 5bfae777f17..e32c4f7415b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -142,6 +142,7 @@ Rails.application.routes.draw do collection do post :create_user post :create_gcp + post :authorize_aws_role end member do diff --git a/db/migrate/20191103202505_add_eks_credentials_to_application_settings.rb b/db/migrate/20191103202505_add_eks_credentials_to_application_settings.rb new file mode 100644 index 00000000000..3a167b4c67f --- /dev/null +++ b/db/migrate/20191103202505_add_eks_credentials_to_application_settings.rb @@ -0,0 +1,14 @@ +# 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 diff --git a/db/schema.rb b/db/schema.rb index 35b6dc00de1..36f82f9913b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" diff --git a/doc/api/settings.md b/doc/api/settings.md index a1c343ec208..a061309d8d1 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -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 | diff --git a/doc/integration/README.md b/doc/integration/README.md index da00cc0ffa0..7a2c9b9bc54 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -2,46 +2,70 @@ comments: false --- -# GitLab Integration - -GitLab integrates with multiple third-party services to allow external issue -trackers and external authentication. - -See the documentation below for details on how to configure these services. - -- [Akismet](akismet.md) Configure Akismet to stop spam -- [Auth0 OmniAuth](auth0.md) Enable the Auth0 OmniAuth provider -- [Bitbucket](bitbucket.md) Import projects from Bitbucket.org and login to your GitLab instance with your Bitbucket.org account -- [CAS](cas.md) Configure GitLab to sign in using CAS -- [External issue tracker](external-issue-tracker.md) Redmine, Jira, etc. -- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages -- [Jenkins](jenkins.md) Integrate with the Jenkins CI -- [Jira](../user/project/integrations/jira.md) Integrate with the Jira issue tracker -- [Kerberos](kerberos.md) Integrate with Kerberos -- [LDAP](ldap.md) Set up sign in via LDAP -- [OAuth2 provider](oauth_provider.md) OAuth2 application creation -- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID -- [OpenID Connect](openid_connect_provider.md) Use GitLab as an identity provider -- [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents. -- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users -- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider -- [Sentry](../user/project/operations/error_tracking.md#sentry-error-tracking) Enable issue linking from Sentry and view Sentry crash reports in GitLab -- [Trello](trello_power_up.md) Integrate Trello with GitLab - -> GitLab Enterprise Edition contains [advanced Jenkins support](jenkins.md). +# GitLab integrations + +GitLab can be integrated with external services for enhanced functionality. + +## Issue trackers + +You can use an [external issue tracker](external-issue-tracker.md) at the same time as the GitLab issue tracker, or use only the external issue tracker. + +GitLab can be integrated with the following external issue trackers: + +- Jira +- Redmine +- Bugzilla +- YouTrack + +## Authentication sources + +GitLab can be configured to authenticate access requests with the following authentication sources: + +- Enable the [Auth0 OmniAuth](auth0.md) provider. +- Enable sign in with [Bitbucket](bitbucket.md) accounts. +- Configure GitLab to sign in using [CAS](cas.md). +- Integrate with [Kerberos](kerberos.md). +- Enable sign in via [LDAP](ldap.md). +- Enable [OAuth2 provider](oauth_provider.md) application creation. +- Use [OmniAuth](omniauth.md) to enable sign in via Twitter, GitHub, GitLab.com, Google, +Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure or Authentiq ID. +- Use GitLab as an [OpenID Connect](openid_connect_provider.md) identity provider. +- Configure GitLab as a [SAML](saml.md) 2.0 Service Provider. + +## Security enhancements + +GitLab can be integrated with the following external services to enhance security: + +- [Akismet](akismet.md) helps reduce spam. +- Google [reCAPTCHA](recaptcha.md) helps verify new users. + +GitLab also provides features to improve the security of your own application. For more details see [GitLab Secure](../user/application_security/index.md). + +## Continuous integration + +GitLab can be integrated with the following external service for continuous integration: + +- [Jenkins](jenkins.md) CI. **(STARTER)** + +## Feature enhancements + +GitLab can be integrated with the following enhancements: + +- Add GitLab actions to [Gmail actions buttons](gmail_action_buttons_for_gitlab.md). +- Configure [PlantUML](../administration/integration/plantuml.md) to use diagrams in AsciiDoc documents. +- Attach merge requests to [Trello](trello_power_up.md) cards. ## Project services -Integration with services such as Campfire, Flowdock, HipChat, -Pivotal Tracker, and Slack are available in the form of a [Project Service][]. +Integration with services such as Campfire, Flowdock, HipChat, Pivotal Tracker, and Slack are available as [Project Services](../user/project/integrations/project_services.md). + +## Troubleshooting -[Project Service]: ../user/project/integrations/project_services.md +### SSL certificate errors -## SSL certificate errors +When trying to integrate GitLab with services that are using self-signed certificates, it is very likely that SSL certificate errors will occur in different parts of the application, most likely Sidekiq. -When trying to integrate GitLab with services that are using self-signed certificates, -it is very likely that SSL certificate errors will occur on different parts of the -application, most likely Sidekiq. There are 2 approaches you can take to solve this: +There are two approaches you can take to solve this: 1. Add the root certificate to the trusted chain of the OS. 1. If using Omnibus, you can add the certificate to GitLab's trusted certificates. @@ -62,12 +86,12 @@ in to GitLab Omnibus. It is enough to concatenate the certificate to the main trusted certificate however it may be overwritten during upgrades: -```bash +```shell cat jira.pem >> /opt/gitlab/embedded/ssl/certs/cacert.pem ``` After that restart GitLab with: -```bash +```shell sudo gitlab-ctl restart ``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 93982d7eefc..e6429725757 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -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 diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 0772423dcb7..11eece8f862 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -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.' diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 607f5321856..e3a434dfe35 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f48df8b2b60..db409df295b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index 88069df6ade..86de421ba3f 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -64,7 +64,13 @@ module QA end def visit! - visit(web_url) + Runtime::Logger.debug("Visiting #{web_url}") + + Support::Retrier.retry_until do + visit(web_url) + + wait { current_url == web_url } + end end def populate(*attributes) @@ -72,7 +78,9 @@ module QA end def wait(max: 60, interval: 0.1) - QA::Support::Waiter.wait(max: max, interval: interval) + QA::Support::Waiter.wait(max: max, interval: interval) do + yield + end end private diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index 4a6b76c869f..fe84b3d024a 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -269,6 +269,8 @@ describe QA::Resource::Base do end it 'calls #visit with the underlying #web_url' do + allow(resource).to receive(:current_url).and_return(subject.current_url) + resource.web_url = subject.current_url resource.visit! diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb index 233710b9fc3..d3192593a78 100644 --- a/spec/controllers/admin/clusters_controller_spec.rb +++ b/spec/controllers/admin/clusters_controller_spec.rb @@ -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) } diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index 51a6dcca640..538a270f567 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -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]) } diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index e1f6d571d27..1b6b0ff025e 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -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]) } diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index e4eddb87858..d340cec8b70 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -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], diff --git a/spec/features/projects/clusters/eks_spec.rb b/spec/features/projects/clusters/eks_spec.rb index 9cb989b8004..e0ebccd85ac 100644 --- a/spec/features/projects/clusters/eks_spec.rb +++ b/spec/features/projects/clusters/eks_spec.rb @@ -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 diff --git a/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js b/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js new file mode 100644 index 00000000000..4bf3ac430f5 --- /dev/null +++ b/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js @@ -0,0 +1,91 @@ +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, + ); + }); + }); + }); +}); diff --git a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js new file mode 100644 index 00000000000..0be723b48f0 --- /dev/null +++ b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js @@ -0,0 +1,117 @@ +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); + }); + }); +}); diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js index 1ed7f806804..99c8cdba296 100644 --- a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js +++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js @@ -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 }]); + }); }); }); diff --git a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js index 81b65180fb5..2637b4822a5 100644 --- a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js +++ b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js @@ -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); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/slot_switch_spec.js b/spec/frontend/vue_shared/components/slot_switch_spec.js new file mode 100644 index 00000000000..cff955c05b2 --- /dev/null +++ b/spec/frontend/vue_shared/components/slot_switch_spec.js @@ -0,0 +1,56 @@ +import { shallowMount } from '@vue/test-utils'; + +import SlotSwitch from '~/vue_shared/components/slot_switch'; + +describe('SlotSwitch', () => { + const slots = { + first: '<a>AGP</a>', + second: '<p>PCI</p>', + }; + + let wrapper; + + const createComponent = propsData => { + wrapper = shallowMount(SlotSwitch, { + propsData, + slots, + sync: false, + }); + }; + + const getChildrenHtml = () => wrapper.findAll('* *').wrappers.map(c => c.html()); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + it('throws an error if activeSlotNames is missing', () => { + expect(createComponent).toThrow('[Vue warn]: Missing required prop: "activeSlotNames"'); + }); + + it('renders no slots if activeSlotNames is empty', () => { + createComponent({ + activeSlotNames: [], + }); + + expect(getChildrenHtml().length).toBe(0); + }); + + it('renders one slot if activeSlotNames contains single slot name', () => { + createComponent({ + activeSlotNames: ['first'], + }); + + expect(getChildrenHtml()).toEqual([slots.first]); + }); + + it('renders multiple slots if activeSlotNames contains multiple slot names', () => { + createComponent({ + activeSlotNames: Object.keys(slots), + }); + + expect(getChildrenHtml()).toEqual(Object.values(slots)); + }); +}); diff --git a/spec/helpers/clusters_helper_spec.rb b/spec/helpers/clusters_helper_spec.rb index 4ea0f76fc28..1ee638ddf04 100644 --- a/spec/helpers/clusters_helper_spec.rb +++ b/spec/helpers/clusters_helper_spec.rb @@ -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 diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 4678adcd85f..1397add9f5a 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -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') } diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index b6fc152e478..9d3f5b4b132 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -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') diff --git a/spec/models/aws/role_spec.rb b/spec/models/aws/role_spec.rb index c40752e40a6..d4165567146 100644 --- a/spec/models/aws/role_spec.rb +++ b/spec/models/aws/role_spec.rb @@ -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 diff --git a/spec/presenters/group_clusterable_presenter_spec.rb b/spec/presenters/group_clusterable_presenter_spec.rb index fa77273f6aa..11a8decc9cc 100644 --- a/spec/presenters/group_clusterable_presenter_spec.rb +++ b/spec/presenters/group_clusterable_presenter_spec.rb @@ -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 } diff --git a/spec/presenters/project_clusterable_presenter_spec.rb b/spec/presenters/project_clusterable_presenter_spec.rb index 6786a84243f..441c2a50fea 100644 --- a/spec/presenters/project_clusterable_presenter_spec.rb +++ b/spec/presenters/project_clusterable_presenter_spec.rb @@ -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 } diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 760eafe33e4..5aba798f2c2 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -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 } diff --git a/spec/services/clusters/aws/fetch_credentials_service_spec.rb b/spec/services/clusters/aws/fetch_credentials_service_spec.rb index 6476130ab32..09f3be0534d 100644 --- a/spec/services/clusters/aws/fetch_credentials_service_spec.rb +++ b/spec/services/clusters/aws/fetch_credentials_service_spec.rb @@ -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) |