diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-12 00:07:43 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-12 00:07:43 +0000 |
commit | 2e3cbf7d89815e2915f77677388c49b48f8d20c3 (patch) | |
tree | 03bdbc99e829295e8077b2ec4032300c15b48e37 /app | |
parent | e44bb86539a8fb4cfb06dfe281632b6f206bd0a7 (diff) | |
download | gitlab-ce-2e3cbf7d89815e2915f77677388c49b48f8d20c3.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
26 files changed, 291 insertions, 327 deletions
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue index 641343b8150..d04d0ff2a6d 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue @@ -22,10 +22,7 @@ const { mapState: mapSecurityGroupsState, mapActions: mapSecurityGroupsActions, } = createNamespacedHelpers('securityGroups'); -const { - mapState: mapInstanceTypesState, - mapActions: mapInstanceTypesActions, -} = createNamespacedHelpers('instanceTypes'); +const { mapState: mapInstanceTypesState } = createNamespacedHelpers('instanceTypes'); export default { components: { @@ -265,12 +262,10 @@ export default { mounted() { this.fetchRegions(); this.fetchRoles(); - this.fetchInstanceTypes(); }, methods: { ...mapActions([ 'createCluster', - 'signOut', 'setClusterName', 'setEnvironmentScope', 'setKubernetesVersion', @@ -290,7 +285,6 @@ export default { ...mapRolesActions({ fetchRoles: 'fetchItems' }), ...mapKeyPairsActions({ fetchKeyPairs: 'fetchItems' }), ...mapSecurityGroupsActions({ fetchSecurityGroups: 'fetchItems' }), - ...mapInstanceTypesActions({ fetchInstanceTypes: 'fetchItems' }), setRegionAndFetchVpcsAndKeyPairs(region) { this.setRegion({ region }); this.setVpc({ vpc: null }); @@ -316,11 +310,6 @@ export default { {{ s__('ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster') }} </h2> <div class="mb-3" v-html="kubernetesIntegrationHelpText"></div> - <div class="mb-3"> - <button class="btn btn-link js-sign-out" @click.prevent="signOut()"> - {{ s__('ClusterIntegration|Select a different AWS role') }} - </button> - </div> <div class="form-group"> <label class="label-bold" for="eks-cluster-name">{{ s__('ClusterIntegration|Kubernetes cluster name') 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 50a23536451..1dd4c468ae6 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 @@ -28,7 +28,7 @@ export default { }, data() { return { - roleArn: '', + roleArn: this.$store.state.roleArn, }; }, computed: { diff --git a/app/assets/javascripts/create_cluster/eks_cluster/index.js b/app/assets/javascripts/create_cluster/eks_cluster/index.js index 27f859d8972..fb993a7aa59 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/index.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/index.js @@ -12,20 +12,14 @@ export default el => { kubernetesIntegrationHelpPath, accountAndExternalIdsHelpPath, createRoleArnHelpPath, - getRolesPath, - getRegionsPath, - getKeyPairsPath, - getVpcsPath, - getSubnetsPath, - getSecurityGroupsPath, - getInstanceTypesPath, externalId, accountId, + instanceTypes, hasCredentials, createRolePath, createClusterPath, - signOutPath, externalLinkIcon, + roleArn, } = el.dataset; return new Vue({ @@ -35,18 +29,10 @@ export default el => { hasCredentials: parseBoolean(hasCredentials), externalId, accountId, + instanceTypes: JSON.parse(instanceTypes), createRolePath, createClusterPath, - signOutPath, - }, - apiPaths: { - getRolesPath, - getRegionsPath, - getKeyPairsPath, - getVpcsPath, - getSubnetsPath, - getSecurityGroupsPath, - getInstanceTypesPath, + roleArn, }, }), components: { diff --git a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js index 21b87d525cf..601ff6f9adc 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js @@ -1,58 +1,98 @@ -import axios from '~/lib/utils/axios_utils'; - -export default apiPaths => ({ - fetchRoles() { - return axios - .get(apiPaths.getRolesPath) - .then(({ data: { roles } }) => - roles.map(({ role_name: name, arn: value }) => ({ name, value })), - ); - }, - fetchKeyPairs({ region }) { - return axios - .get(apiPaths.getKeyPairsPath, { params: { region } }) - .then(({ data: { key_pairs: keyPairs } }) => - keyPairs.map(({ key_name }) => ({ name: key_name, value: key_name })), - ); - }, - fetchRegions() { - return axios.get(apiPaths.getRegionsPath).then(({ data: { regions } }) => - regions.map(({ region_name }) => ({ - name: region_name, - value: region_name, +import AWS from 'aws-sdk/global'; +import EC2 from 'aws-sdk/clients/ec2'; +import IAM from 'aws-sdk/clients/iam'; + +const lookupVpcName = ({ Tags: tags, VpcId: id }) => { + const nameTag = tags.find(({ Key: key }) => key === 'Name'); + + return nameTag ? nameTag.Value : id; +}; + +export const DEFAULT_REGION = 'us-east-2'; + +export const setAWSConfig = ({ awsCredentials }) => { + AWS.config = { + ...awsCredentials, + region: DEFAULT_REGION, + }; +}; + +export const fetchRoles = () => { + const iam = new IAM(); + + return iam + .listRoles() + .promise() + .then(({ Roles: roles }) => roles.map(({ RoleName: name, Arn: value }) => ({ name, value }))); +}; + +export const fetchRegions = () => { + const ec2 = new EC2(); + + return ec2 + .describeRegions() + .promise() + .then(({ Regions: regions }) => + regions.map(({ RegionName: name }) => ({ + name, + value: name, })), ); - }, - fetchVpcs({ region }) { - return axios.get(apiPaths.getVpcsPath, { params: { region } }).then(({ data: { vpcs } }) => - vpcs.map(({ vpc_id }) => ({ - value: vpc_id, - name: vpc_id, +}; + +export const fetchKeyPairs = ({ region }) => { + const ec2 = new EC2({ region }); + + return ec2 + .describeKeyPairs() + .promise() + .then(({ KeyPairs: keyPairs }) => keyPairs.map(({ KeyName: name }) => ({ name, value: name }))); +}; + +export const fetchVpcs = ({ region }) => { + const ec2 = new EC2({ region }); + + return ec2 + .describeVpcs() + .promise() + .then(({ Vpcs: vpcs }) => + vpcs.map(vpc => ({ + value: vpc.VpcId, + name: lookupVpcName(vpc), })), ); - }, - fetchSubnets({ vpc, region }) { - return axios - .get(apiPaths.getSubnetsPath, { params: { vpc_id: vpc, region } }) - .then(({ data: { subnets } }) => - subnets.map(({ subnet_id }) => ({ name: subnet_id, value: subnet_id })), - ); - }, - fetchSecurityGroups({ vpc, region }) { - return axios - .get(apiPaths.getSecurityGroupsPath, { params: { vpc_id: vpc, region } }) - .then(({ data: { security_groups: securityGroups } }) => - securityGroups.map(({ group_name: name, group_id: value }) => ({ name, value })), - ); - }, - fetchInstanceTypes() { - return axios - .get(apiPaths.getInstanceTypesPath) - .then(({ data: { instance_types: instanceTypes } }) => - instanceTypes.map(({ instance_type_name }) => ({ - name: instance_type_name, - value: instance_type_name, - })), - ); - }, -}); +}; + +export const fetchSubnets = ({ vpc, region }) => { + const ec2 = new EC2({ region }); + + return ec2 + .describeSubnets({ + Filters: [ + { + Name: 'vpc-id', + Values: [vpc], + }, + ], + }) + .promise() + .then(({ Subnets: subnets }) => subnets.map(({ SubnetId: id }) => ({ value: id, name: id }))); +}; + +export const fetchSecurityGroups = ({ region, vpc }) => { + const ec2 = new EC2({ region }); + + return ec2 + .describeSecurityGroups({ + Filters: [ + { + Name: 'vpc-id', + Values: [vpc], + }, + ], + }) + .promise() + .then(({ SecurityGroups: securityGroups }) => + securityGroups.map(({ GroupName: name, GroupId: value }) => ({ name, value })), + ); +}; 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 72f15263a8f..e96e6d6e4f8 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js @@ -1,6 +1,8 @@ import * as types from './mutation_types'; +import { setAWSConfig } from '../services/aws_services_facade'; import axios from '~/lib/utils/axios_utils'; import createFlash from '~/flash'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; const getErrorMessage = data => { const errorKey = Object.keys(data)[0]; @@ -28,7 +30,7 @@ export const createRole = ({ dispatch, state: { createRolePath } }, payload) => role_arn: payload.roleArn, role_external_id: payload.externalId, }) - .then(() => dispatch('createRoleSuccess')) + .then(({ data }) => dispatch('createRoleSuccess', convertObjectPropsToCamelCase(data))) .catch(error => dispatch('createRoleError', { error })); }; @@ -36,7 +38,8 @@ export const requestCreateRole = ({ commit }) => { commit(types.REQUEST_CREATE_ROLE); }; -export const createRoleSuccess = ({ commit }) => { +export const createRoleSuccess = ({ commit }, awsCredentials) => { + setAWSConfig({ awsCredentials }); commit(types.CREATE_ROLE_SUCCESS); }; @@ -117,9 +120,3 @@ export const setInstanceType = ({ commit }, payload) => { export const setNodeCount = ({ commit }, payload) => { commit(types.SET_NODE_COUNT, payload); }; - -export const signOut = ({ commit, state: { signOutPath } }) => - axios - .delete(signOutPath) - .then(() => commit(types.SIGN_OUT)) - .catch(({ response: { data } }) => createFlash(getErrorMessage(data))); diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/cluster_dropdown/index.js b/app/assets/javascripts/create_cluster/eks_cluster/store/cluster_dropdown/index.js index 07a5821c47d..0b19589215c 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/cluster_dropdown/index.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/cluster_dropdown/index.js @@ -3,11 +3,11 @@ import actions from './actions'; import mutations from './mutations'; import state from './state'; -const createStore = fetchFn => ({ +const createStore = ({ fetchFn, initialState }) => ({ actions: actions(fetchFn), getters, mutations, - state: state(), + state: Object.assign(state(), initialState || {}), }); export default createStore; 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 5982fc8a2fd..09fd560240d 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js @@ -6,12 +6,17 @@ import state from './state'; import clusterDropdownStore from './cluster_dropdown'; -import awsServicesFactory from '../services/aws_services_facade'; +import { + fetchRoles, + fetchRegions, + fetchKeyPairs, + fetchVpcs, + fetchSubnets, + fetchSecurityGroups, +} from '../services/aws_services_facade'; -const createStore = ({ initialState, apiPaths }) => { - const awsServices = awsServicesFactory(apiPaths); - - return new Vuex.Store({ +const createStore = ({ initialState }) => + new Vuex.Store({ actions, getters, mutations, @@ -19,34 +24,33 @@ const createStore = ({ initialState, apiPaths }) => { modules: { roles: { namespaced: true, - ...clusterDropdownStore(awsServices.fetchRoles), + ...clusterDropdownStore({ fetchFn: fetchRoles }), }, regions: { namespaced: true, - ...clusterDropdownStore(awsServices.fetchRegions), + ...clusterDropdownStore({ fetchFn: fetchRegions }), }, keyPairs: { namespaced: true, - ...clusterDropdownStore(awsServices.fetchKeyPairs), + ...clusterDropdownStore({ fetchFn: fetchKeyPairs }), }, vpcs: { namespaced: true, - ...clusterDropdownStore(awsServices.fetchVpcs), + ...clusterDropdownStore({ fetchFn: fetchVpcs }), }, subnets: { namespaced: true, - ...clusterDropdownStore(awsServices.fetchSubnets), + ...clusterDropdownStore({ fetchFn: fetchSubnets }), }, securityGroups: { namespaced: true, - ...clusterDropdownStore(awsServices.fetchSecurityGroups), + ...clusterDropdownStore({ fetchFn: fetchSecurityGroups }), }, instanceTypes: { namespaced: true, - ...clusterDropdownStore(awsServices.fetchInstanceTypes), + ...clusterDropdownStore({ initialState: { items: initialState.instanceTypes } }), }, }, }); -}; export default createStore; 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 f9204cc2207..9dee6abae5f 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 @@ -13,7 +13,6 @@ 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'; -export const SIGN_OUT = 'SIGN_OUT'; export const REQUEST_CREATE_CLUSTER = 'REQUEST_CREATE_CLUSTER'; export const CREATE_CLUSTER_SUCCESS = 'CREATE_CLUSTER_SUCCESS'; export const CREATE_CLUSTER_ERROR = 'CREATE_CLUSTER_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 aa04c8f7079..c331d27d255 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js @@ -60,7 +60,4 @@ export default { state.isCreatingCluster = false; state.createClusterError = error; }, - [types.SIGN_OUT](state) { - 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 2e3a05a9187..20434dcce98 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js @@ -12,6 +12,8 @@ export default () => ({ accountId: '', externalId: '', + roleArn: '', + clusterName: '', environmentScope: '*', kubernetesVersion, diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 33ae778769a..1ed8da57927 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -228,10 +228,6 @@ 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 0295f36732c..f4b74b14c0b 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -8,7 +8,7 @@ class Clusters::ClustersController < Clusters::BaseController 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, :authorize_aws_role, :revoke_aws_role, :aws_proxy] + before_action :authorize_create_cluster!, only: [:new, :authorize_aws_role] before_action :authorize_update_cluster!, only: [:update] before_action :authorize_admin_cluster!, only: [:destroy, :clear_cache] before_action :update_applications_status, only: [:cluster_status] @@ -42,6 +42,7 @@ class Clusters::ClustersController < Clusters::BaseController if params[:provider] == 'aws' @aws_role = current_user.aws_role || Aws::Role.new @aws_role.ensure_role_external_id! + @instance_types = load_instance_types.to_json elsif params[:provider] == 'gcp' redirect_to @authorize_url if @authorize_url && !@valid_gcp_token @@ -145,21 +146,9 @@ class Clusters::ClustersController < Clusters::BaseController end def authorize_aws_role - role = current_user.build_aws_role(create_role_params) - - role.save ? respond_201 : respond_422 - end - - def revoke_aws_role - current_user.aws_role&.destroy - - head :no_content - end - - def aws_proxy - response = Clusters::Aws::ProxyService.new( - current_user.aws_role, - params: params + response = Clusters::Aws::AuthorizeRoleService.new( + current_user, + params: aws_role_params ).execute render json: response.body, status: response.status @@ -268,7 +257,7 @@ class Clusters::ClustersController < Clusters::BaseController ) end - def create_role_params + def aws_role_params params.require(:cluster).permit(:role_arn, :role_external_id) end @@ -314,6 +303,19 @@ class Clusters::ClustersController < Clusters::BaseController end end + ## + # Unfortunately the EC2 API doesn't provide a list of + # possible instance types. There is a workaround, using + # the Pricing API, but instead of requiring the + # user to grant extra permissions for this we use the + # values that validate the CloudFormation template. + def load_instance_types + stack_template = File.read(Rails.root.join('vendor', 'aws', 'cloudformation', 'eks_cluster.yaml')) + instance_types = YAML.safe_load(stack_template).dig('Parameters', 'NodeInstanceType', 'AllowedValues') + + instance_types.map { |type| Hash(name: type, value: type) } + end + def update_applications_status @cluster.applications.each(&:schedule_status_update) end diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb index 56a66dd38db..ba21ccfb169 100644 --- a/app/controllers/projects/error_tracking_controller.rb +++ b/app/controllers/projects/error_tracking_controller.rb @@ -77,8 +77,10 @@ class Projects::ErrorTrackingController < Projects::ApplicationController return if handle_errors(result) + result_with_syntax_highlight = Gitlab::ErrorTracking::StackTraceHighlightDecorator.decorate(result[:latest_event]) + render json: { - error: serialize_error_event(result[:latest_event]) + error: serialize_error_event(result_with_syntax_highlight) } end diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb index 78eb75ddcc0..faf587fb83d 100644 --- a/app/models/clusters/providers/aws.rb +++ b/app/models/clusters/providers/aws.rb @@ -8,9 +8,11 @@ module Clusters self.table_name = 'cluster_providers_aws' + DEFAULT_REGION = 'us-east-1' + belongs_to :cluster, inverse_of: :provider_aws, class_name: 'Clusters::Cluster' - default_value_for :region, 'us-east-1' + default_value_for :region, DEFAULT_REGION default_value_for :num_nodes, 3 default_value_for :instance_type, 'm5.large' diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 19216281e48..793ea3c29c3 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -9,7 +9,7 @@ class DeployKey < Key scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) } scope :are_public, -> { where(public: true) } - scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, :namespace] }) } + scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, namespace: :route] }) } ignore_column :can_push, remove_after: '2019-12-15', remove_with: '12.6' @@ -24,7 +24,7 @@ class DeployKey < Key end def almost_orphaned? - self.deploy_keys_projects.count == 1 + self.deploy_keys_projects.size == 1 end def destroyed_when_orphaned? @@ -44,7 +44,11 @@ class DeployKey < Key end def deploy_keys_project_for(project) - deploy_keys_projects.find_by(project: project) + if association(:deploy_keys_projects).loaded? + deploy_keys_projects.find { |dkp| dkp.project_id.eql?(project&.id) } + else + deploy_keys_projects.find_by(project: project) + end end def projects_with_write_access diff --git a/app/models/user.rb b/app/models/user.rb index 6a29de20d86..698848c5b16 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1001,7 +1001,7 @@ class User < ApplicationRecord end def project_deploy_keys - DeployKey.in_projects(authorized_projects.select(:id)).distinct(:id) + @project_deploy_keys ||= DeployKey.in_projects(authorized_projects.select(:id)).distinct(:id) end def highest_role diff --git a/app/policies/deploy_key_policy.rb b/app/policies/deploy_key_policy.rb index 7f0ec011e79..b117bb57921 100644 --- a/app/policies/deploy_key_policy.rb +++ b/app/policies/deploy_key_policy.rb @@ -3,10 +3,7 @@ class DeployKeyPolicy < BasePolicy with_options scope: :subject, score: 0 condition(:private_deploy_key) { @subject.private? } - - # rubocop: disable CodeReuse/ActiveRecord - condition(:has_deploy_key) { @user.project_deploy_keys.exists?(id: @subject.id) } - # rubocop: enable CodeReuse/ActiveRecord + condition(:has_deploy_key) { @user.project_deploy_keys.any? { |pdk| pdk.id.eql?(@subject.id) } } rule { anonymous }.prevent_all diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb index 7677e6f026f..6b1d82e7557 100644 --- a/app/presenters/clusterable_presenter.rb +++ b/app/presenters/clusterable_presenter.rb @@ -29,18 +29,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated new_polymorphic_path([clusterable, :cluster], options) end - def aws_api_proxy_path(resource) - polymorphic_path([clusterable, :clusters], action: :aws_proxy, resource: resource) - end - def authorize_aws_role_path polymorphic_path([clusterable, :clusters], action: :authorize_aws_role) end - def revoke_aws_role_path - polymorphic_path([clusterable, :clusters], action: :revoke_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 34d3f347689..0c267fd5735 100644 --- a/app/presenters/instance_clusterable_presenter.rb +++ b/app/presenters/instance_clusterable_presenter.rb @@ -67,16 +67,6 @@ class InstanceClusterablePresenter < ClusterablePresenter authorize_aws_role_admin_clusters_path end - override :revoke_aws_role_path - def revoke_aws_role_path - revoke_aws_role_admin_clusters_path - end - - override :aws_api_proxy_path - def aws_api_proxy_path(resource) - aws_proxy_admin_clusters_path(resource: resource) - 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/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb index 9bb7fe13593..66211d02696 100644 --- a/app/presenters/projects/settings/deploy_keys_presenter.rb +++ b/app/presenters/projects/settings/deploy_keys_presenter.rb @@ -3,6 +3,8 @@ module Projects module Settings class DeployKeysPresenter < Gitlab::View::Presenter::Simple + include Gitlab::Utils::StrongMemoize + presents :project delegate :size, to: :enabled_keys, prefix: true delegate :size, to: :available_project_keys, prefix: true @@ -13,37 +15,45 @@ module Projects end def enabled_keys - project.deploy_keys + strong_memoize(:enabled_keys) do + project.deploy_keys.with_projects + end end def available_keys - current_user - .accessible_deploy_keys - .id_not_in(enabled_keys.select(:id)) - .with_projects + strong_memoize(:available_keys) do + current_user + .accessible_deploy_keys + .id_not_in(enabled_keys.select(:id)) + .with_projects + end end def available_project_keys - current_user - .project_deploy_keys - .id_not_in(enabled_keys.select(:id)) - .with_projects + strong_memoize(:available_project_keys) do + current_user + .project_deploy_keys + .id_not_in(enabled_keys.select(:id)) + .with_projects + end end def available_public_keys - DeployKey - .are_public - .id_not_in(enabled_keys.select(:id)) - .id_not_in(available_project_keys.select(:id)) - .with_projects + strong_memoize(:available_public_keys) do + DeployKey + .are_public + .id_not_in(enabled_keys.select(:id)) + .id_not_in(available_project_keys.select(:id)) + .with_projects + end end def as_json serializer = DeployKeySerializer.new # rubocop: disable CodeReuse/Serializer - opts = { user: current_user, project: project } + opts = { user: current_user, project: project, readable_project_ids: readable_project_ids } { - enabled_keys: serializer.represent(enabled_keys.with_projects, opts), + enabled_keys: serializer.represent(enabled_keys, opts), available_project_keys: serializer.represent(available_project_keys, opts), public_keys: serializer.represent(available_public_keys, opts) } @@ -56,6 +66,26 @@ module Projects def form_partial_path 'projects/deploy_keys/form' end + + private + + # Caching all readable project ids for the user that are associated with the queried deploy keys + def readable_project_ids + strong_memoize(:readable_projects_by_id) do + Set.new(user_readable_project_ids) + end + end + + # rubocop: disable CodeReuse/ActiveRecord + def user_readable_project_ids + project_ids = (available_keys + available_project_keys + available_public_keys) + .flat_map { |deploy_key| deploy_key.deploy_keys_projects.map(&:project_id) } + .compact + .uniq + + current_user.authorized_projects(Gitlab::Access::GUEST).id_in(project_ids).pluck(:id) + end + # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/serializers/deploy_key_entity.rb b/app/serializers/deploy_key_entity.rb index 9a558d12bec..2682a47fbaa 100644 --- a/app/serializers/deploy_key_entity.rb +++ b/app/serializers/deploy_key_entity.rb @@ -11,8 +11,7 @@ class DeployKeyEntity < Grape::Entity expose :updated_at expose :deploy_keys_projects, using: DeployKeysProjectEntity do |deploy_key| deploy_key.deploy_keys_projects.select do |deploy_key_project| - !deploy_key_project.project&.pending_delete? && - Ability.allowed?(options[:user], :read_project, deploy_key_project.project) + !deploy_key_project.project&.pending_delete? && (allowed_to_read_project?(deploy_key_project.project) || options[:user].admin?) end end expose :can_edit @@ -23,4 +22,12 @@ class DeployKeyEntity < Grape::Entity Ability.allowed?(options[:user], :update_deploy_key, object) || Ability.allowed?(options[:user], :update_deploy_keys_project, object.deploy_keys_project_for(options[:project])) end + + def allowed_to_read_project?(project) + if options[:readable_project_ids] + options[:readable_project_ids].include?(project.id) + else + Ability.allowed?(options[:user], :read_project, project) + end + end end diff --git a/app/services/clusters/aws/authorize_role_service.rb b/app/services/clusters/aws/authorize_role_service.rb new file mode 100644 index 00000000000..6eafce0597e --- /dev/null +++ b/app/services/clusters/aws/authorize_role_service.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Clusters + module Aws + class AuthorizeRoleService + attr_reader :user + + Response = Struct.new(:status, :body) + + ERRORS = [ + ActiveRecord::RecordInvalid, + Clusters::Aws::FetchCredentialsService::MissingRoleError, + ::Aws::Errors::MissingCredentialsError, + ::Aws::STS::Errors::ServiceError + ].freeze + + def initialize(user, params:) + @user = user + @params = params + end + + def execute + @role = create_or_update_role! + + Response.new(:ok, credentials) + rescue *ERRORS + Response.new(:unprocessable_entity, {}) + end + + private + + attr_reader :role, :params + + def create_or_update_role! + if role = user.aws_role + role.update!(params) + + role + else + user.create_aws_role!(params) + end + end + + def credentials + Clusters::Aws::FetchCredentialsService.new(role).execute + end + end + end +end diff --git a/app/services/clusters/aws/fetch_credentials_service.rb b/app/services/clusters/aws/fetch_credentials_service.rb index 2724d4b657b..33efc4cc120 100644 --- a/app/services/clusters/aws/fetch_credentials_service.rb +++ b/app/services/clusters/aws/fetch_credentials_service.rb @@ -7,9 +7,8 @@ module Clusters MissingRoleError = Class.new(StandardError) - def initialize(provision_role, region:, provider: nil) + def initialize(provision_role, provider: nil) @provision_role = provision_role - @region = region @provider = provider end @@ -20,13 +19,14 @@ module Clusters client: client, role_arn: provision_role.role_arn, role_session_name: session_name, - external_id: provision_role.role_external_id + external_id: provision_role.role_external_id, + policy: session_policy ).credentials end private - attr_reader :provider, :region + attr_reader :provider def client ::Aws::STS::Client.new(credentials: gitlab_credentials, region: region) @@ -44,6 +44,26 @@ module Clusters Gitlab::CurrentSettings.eks_secret_access_key end + def region + provider&.region || Clusters::Providers::Aws::DEFAULT_REGION + end + + ## + # If we haven't created a provider record yet, + # we restrict ourselves to read only access so + # that we can safely expose credentials to the + # frontend (to be used when populating the + # creation form). + def session_policy + if provider.nil? + File.read(read_only_policy) + end + end + + def read_only_policy + Rails.root.join('vendor', 'aws', 'iam', "eks_cluster_read_only_policy.json") + end + def session_name if provider.present? "gitlab-eks-cluster-#{provider.cluster_id}-user-#{provision_role.user_id}" diff --git a/app/services/clusters/aws/proxy_service.rb b/app/services/clusters/aws/proxy_service.rb deleted file mode 100644 index df8fc480005..00000000000 --- a/app/services/clusters/aws/proxy_service.rb +++ /dev/null @@ -1,134 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Aws - class ProxyService - DEFAULT_REGION = 'us-east-1' - - BadRequest = Class.new(StandardError) - Response = Struct.new(:status, :body) - - def initialize(role, params:) - @role = role - @params = params - end - - def execute - api_response = request_from_api! - - Response.new(:ok, api_response.to_hash) - rescue *service_errors - Response.new(:bad_request, {}) - end - - private - - attr_reader :role, :params - - def request_from_api! - case requested_resource - when 'key_pairs' - ec2_client.describe_key_pairs - - when 'instance_types' - instance_types - - when 'roles' - iam_client.list_roles - - when 'regions' - ec2_client.describe_regions - - when 'security_groups' - raise BadRequest unless vpc_id.present? - - ec2_client.describe_security_groups(vpc_filter) - - when 'subnets' - raise BadRequest unless vpc_id.present? - - ec2_client.describe_subnets(vpc_filter) - - when 'vpcs' - ec2_client.describe_vpcs - - else - raise BadRequest - end - end - - def requested_resource - params[:resource] - end - - def vpc_id - params[:vpc_id] - end - - def region - params[:region] || DEFAULT_REGION - end - - def vpc_filter - { - filters: [{ - name: "vpc-id", - values: [vpc_id] - }] - } - end - - ## - # Unfortunately the EC2 API doesn't provide a list of - # possible instance types. There is a workaround, using - # the Pricing API, but instead of requiring the - # user to grant extra permissions for this we use the - # values that validate the CloudFormation template. - def instance_types - { - instance_types: cluster_stack_instance_types.map { |type| Hash(instance_type_name: type) } - } - end - - def cluster_stack_instance_types - YAML.safe_load(stack_template).dig('Parameters', 'NodeInstanceType', 'AllowedValues') - end - - def stack_template - File.read(Rails.root.join('vendor', 'aws', 'cloudformation', 'eks_cluster.yaml')) - end - - def ec2_client - ::Aws::EC2::Client.new(client_options) - end - - def iam_client - ::Aws::IAM::Client.new(client_options) - end - - def credentials - Clusters::Aws::FetchCredentialsService.new(role, region: region).execute - end - - def client_options - { - credentials: credentials, - region: region, - http_open_timeout: 5, - http_read_timeout: 10 - } - end - - def service_errors - [ - BadRequest, - Clusters::Aws::FetchCredentialsService::MissingRoleError, - ::Aws::Errors::MissingCredentialsError, - ::Aws::EC2::Errors::ServiceError, - ::Aws::IAM::Errors::ServiceError, - ::Aws::STS::Errors::ServiceError - ] - end - end - end -end diff --git a/app/services/clusters/kubernetes/kubernetes.rb b/app/services/clusters/kubernetes.rb index 59cb1c4b3a9..59cb1c4b3a9 100644 --- a/app/services/clusters/kubernetes/kubernetes.rb +++ b/app/services/clusters/kubernetes.rb diff --git a/app/views/clusters/clusters/aws/_new.html.haml b/app/views/clusters/clusters/aws/_new.html.haml index 795b80bfb6f..d89e6965dac 100644 --- a/app/views/clusters/clusters/aws/_new.html.haml +++ b/app/views/clusters/clusters/aws/_new.html.haml @@ -5,19 +5,12 @@ - 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, - 'sign-out-path' => clusterable.revoke_aws_role_path, 'create-cluster-path' => clusterable.create_aws_clusters_path, - 'get-roles-path' => clusterable.aws_api_proxy_path('roles'), - 'get-regions-path' => clusterable.aws_api_proxy_path('regions'), - 'get-key-pairs-path' => clusterable.aws_api_proxy_path('key_pairs'), - 'get-vpcs-path' => clusterable.aws_api_proxy_path('vpcs'), - 'get-subnets-path' => clusterable.aws_api_proxy_path('subnets'), - 'get-security-groups-path' => clusterable.aws_api_proxy_path('security_groups'), - 'get-instance-types-path' => clusterable.aws_api_proxy_path('instance_types'), 'account-id' => Gitlab::CurrentSettings.eks_account_id, 'external-id' => @aws_role.role_external_id, + 'role-arn' => @aws_role.role_arn, + 'instance-types' => @instance_types, 'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index'), 'account-and-external-ids-help-path' => help_page_path('user/project/clusters/add_remove_clusters.md', anchor: 'eks-cluster'), 'create-role-arn-help-path' => help_page_path('user/project/clusters/add_remove_clusters.md', anchor: 'eks-cluster'), - 'external-link-icon' => icon('external-link'), - 'has-credentials' => @aws_role.role_arn.present?.to_s } } + 'external-link-icon' => icon('external-link') } } |