Commit 5f329c69 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 0dda42b266f0
<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: '*',
......
<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>
......@@ -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 @@ def render_404
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 @@ def index
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 @@ def create_user
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 @@ def create_user_cluster_params
)
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 @@ def visible_attributes
: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 @@ def has_multiple_clusters?
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 @@ def defaults
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 @@ class Role < ApplicationRecord
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 @@ def new_path(options = {})
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 @@ def create_gcp_clusters_path
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 @@ def gitlab_credentials
::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