diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-01 03:06:26 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-01 03:06:26 +0000 |
commit | 56d96ad7fab4d4b95f5529d8080b3cc2873794a0 (patch) | |
tree | 7fe93fc8ff4d82d815000781ffb9c98d7259211a /app | |
parent | 8078bd185fd9fce86cb5a8d9a6b6209e0c23ae44 (diff) | |
download | gitlab-ce-56d96ad7fab4d4b95f5529d8080b3cc2873794a0.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r-- | app/controllers/projects/releases_controller.rb | 23 | ||||
-rw-r--r-- | app/finders/releases_finder.rb | 6 | ||||
-rw-r--r-- | app/models/clusters/providers/aws.rb | 13 | ||||
-rw-r--r-- | app/models/release.rb | 6 | ||||
-rw-r--r-- | app/presenters/release_presenter.rb | 10 | ||||
-rw-r--r-- | app/services/clusters/aws/fetch_credentials_service.rb | 60 | ||||
-rw-r--r-- | app/services/clusters/aws/finalize_creation_service.rb | 139 | ||||
-rw-r--r-- | app/services/clusters/aws/provision_service.rb | 77 | ||||
-rw-r--r-- | app/services/clusters/aws/verify_provision_status_service.rb | 50 | ||||
-rw-r--r-- | app/workers/cluster_provision_worker.rb | 6 | ||||
-rw-r--r-- | app/workers/new_release_worker.rb | 2 | ||||
-rw-r--r-- | app/workers/wait_for_cluster_creation_worker.rb | 6 |
12 files changed, 392 insertions, 6 deletions
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 0f9cb2dfc31..40e467e9e8a 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -3,10 +3,12 @@ class Projects::ReleasesController < Projects::ApplicationController # Authorize before_action :require_non_empty_project, except: [:index] + before_action :release, only: %i[edit update] before_action :authorize_read_release! before_action do push_frontend_feature_flag(:release_edit_page, project) end + before_action :authorize_update_release!, only: %i[edit update] def index respond_to do |format| @@ -22,4 +24,25 @@ class Projects::ReleasesController < Projects::ApplicationController def releases ReleasesFinder.new(@project, current_user).execute end + + def edit + respond_to do |format| + format.html { render 'edit' } + end + end + + private + + def authorize_update_release! + access_denied! unless Feature.enabled?(:release_edit_page, project) + access_denied! unless can?(current_user, :update_release, release) + end + + def release + @release ||= project.releases.find_by_tag!(sanitized_tag_name) + end + + def sanitized_tag_name + CGI.unescape(params[:tag]) + end end diff --git a/app/finders/releases_finder.rb b/app/finders/releases_finder.rb index 59e84198fde..72bf968c8ec 100644 --- a/app/finders/releases_finder.rb +++ b/app/finders/releases_finder.rb @@ -6,9 +6,11 @@ class ReleasesFinder @current_user = current_user end - def execute + def execute(preload: true) return Release.none unless Ability.allowed?(@current_user, :read_release, @project) - @project.releases.sorted + releases = @project.releases + releases = releases.preloaded if preload + releases.sorted end end diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb index ae4156896bc..89eb56aa41f 100644 --- a/app/models/clusters/providers/aws.rb +++ b/app/models/clusters/providers/aws.rb @@ -3,6 +3,7 @@ module Clusters module Providers class Aws < ApplicationRecord + include Gitlab::Utils::StrongMemoize include Clusters::Concerns::ProviderStatus self.table_name = 'cluster_providers_aws' @@ -42,6 +43,18 @@ module Clusters session_token: nil ) end + + def api_client + strong_memoize(:api_client) do + ::Aws::CloudFormation::Client.new(credentials: credentials, region: region) + end + end + + def credentials + strong_memoize(:credentials) do + ::Aws::Credentials.new(access_key_id, secret_access_key, session_token) + end + end end end end diff --git a/app/models/release.rb b/app/models/release.rb index a069e1523b8..1261402712f 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -27,13 +27,17 @@ class Release < ApplicationRecord validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } scope :sorted, -> { order(released_at: :desc) } - scope :with_project_and_namespace, -> { includes(project: :namespace) } + scope :preloaded, -> { includes(project: :namespace) } delegate :repository, to: :project after_commit :create_evidence!, on: :create after_commit :notify_new_release, on: :create + def to_param + CGI.escape(tag) + end + def commit strong_memoize(:commit) do repository.commit(actual_sha) diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb index de9055db1e5..7a4d6f5c4c4 100644 --- a/app/presenters/release_presenter.rb +++ b/app/presenters/release_presenter.rb @@ -31,6 +31,12 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated project_issues_url(project, params_for_issues_and_mrs) end + def edit_url + return unless release_edit_page_available? + + edit_project_release_url(project, release) + end + private def can_download_code? @@ -44,4 +50,8 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated def release_mr_issue_urls_available? ::Feature.enabled?(:release_mr_issue_urls, project) end + + def release_edit_page_available? + ::Feature.enabled?(:release_edit_page, project) + end end diff --git a/app/services/clusters/aws/fetch_credentials_service.rb b/app/services/clusters/aws/fetch_credentials_service.rb new file mode 100644 index 00000000000..e1d04fac976 --- /dev/null +++ b/app/services/clusters/aws/fetch_credentials_service.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Clusters + module Aws + class FetchCredentialsService + attr_reader :provider + + MissingRoleError = Class.new(StandardError) + + def initialize(provider) + @provider = provider + end + + def execute + raise MissingRoleError.new('AWS provisioning role not configured') unless provision_role.present? + + ::Aws::AssumeRoleCredentials.new( + client: client, + role_arn: provision_role.role_arn, + role_session_name: session_name, + external_id: provision_role.role_external_id + ).credentials + end + + private + + def provision_role + provider.created_by_user.aws_role + end + + def client + ::Aws::STS::Client.new(credentials: gitlab_credentials, region: provider.region) + end + + 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 + 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 + end + + def session_name + "gitlab-eks-cluster-#{provider.cluster_id}-user-#{provider.created_by_user_id}" + end + end + end +end diff --git a/app/services/clusters/aws/finalize_creation_service.rb b/app/services/clusters/aws/finalize_creation_service.rb new file mode 100644 index 00000000000..54f07e1d44c --- /dev/null +++ b/app/services/clusters/aws/finalize_creation_service.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +module Clusters + module Aws + class FinalizeCreationService + include Gitlab::Utils::StrongMemoize + + attr_reader :provider + + delegate :cluster, to: :provider + + def execute(provider) + @provider = provider + + configure_provider + create_gitlab_service_account! + configure_platform_kubernetes + configure_node_authentication! + + cluster.save! + rescue ::Aws::CloudFormation::Errors::ServiceError => e + log_service_error(e.class.name, provider.id, e.message) + provider.make_errored!(s_('ClusterIntegration|Failed to fetch CloudFormation stack: %{message}') % { message: e.message }) + rescue Kubeclient::HttpError => e + log_service_error(e.class.name, provider.id, e.message) + provider.make_errored!(s_('ClusterIntegration|Failed to run Kubeclient: %{message}') % { message: e.message }) + rescue ActiveRecord::RecordInvalid => e + log_service_error(e.class.name, provider.id, e.message) + provider.make_errored!(s_('ClusterIntegration|Failed to configure EKS provider: %{message}') % { message: e.message }) + end + + private + + def create_gitlab_service_account! + Clusters::Kubernetes::CreateOrUpdateServiceAccountService.gitlab_creator( + kube_client, + rbac: true + ).execute + end + + def configure_provider + provider.status_event = :make_created + end + + def configure_platform_kubernetes + cluster.build_platform_kubernetes( + api_url: cluster_endpoint, + ca_cert: cluster_certificate, + token: request_kubernetes_token) + end + + def request_kubernetes_token + Clusters::Kubernetes::FetchKubernetesTokenService.new( + kube_client, + Clusters::Kubernetes::GITLAB_ADMIN_TOKEN_NAME, + Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE + ).execute + end + + def kube_client + @kube_client ||= build_kube_client!( + cluster_endpoint, + cluster_certificate + ) + end + + def build_kube_client!(api_url, ca_pem) + raise "Incomplete settings" unless api_url + + Gitlab::Kubernetes::KubeClient.new( + api_url, + auth_options: kubeclient_auth_options, + ssl_options: kubeclient_ssl_options(ca_pem), + http_proxy_uri: ENV['http_proxy'] + ) + end + + def kubeclient_auth_options + { bearer_token: Kubeclient::AmazonEksCredentials.token(provider.credentials, cluster.name) } + end + + def kubeclient_ssl_options(ca_pem) + opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } + + if ca_pem.present? + opts[:cert_store] = OpenSSL::X509::Store.new + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + end + + opts + end + + def cluster_stack + @cluster_stack ||= provider.api_client.describe_stacks(stack_name: provider.cluster.name).stacks.first + end + + def stack_output_value(key) + cluster_stack.outputs.detect { |output| output.output_key == key }.output_value + end + + def node_instance_role_arn + stack_output_value('NodeInstanceRole') + end + + def cluster_endpoint + strong_memoize(:cluster_endpoint) do + stack_output_value('ClusterEndpoint') + end + end + + def cluster_certificate + strong_memoize(:cluster_certificate) do + Base64.decode64(stack_output_value('ClusterCertificate')) + end + end + + def configure_node_authentication! + kube_client.create_config_map(node_authentication_config) + end + + def node_authentication_config + Gitlab::Kubernetes::ConfigMaps::AwsNodeAuth.new(node_instance_role_arn).generate + end + + def logger + @logger ||= Gitlab::Kubernetes::Logger.build + end + + def log_service_error(exception, provider_id, message) + logger.error( + exception: exception.class.name, + service: self.class.name, + provider_id: provider_id, + message: message + ) + end + end + end +end diff --git a/app/services/clusters/aws/provision_service.rb b/app/services/clusters/aws/provision_service.rb new file mode 100644 index 00000000000..6a3b0dffae2 --- /dev/null +++ b/app/services/clusters/aws/provision_service.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Clusters + module Aws + class ProvisionService + attr_reader :provider + + def execute(provider) + @provider = provider + + configure_provider_credentials + provision_cluster + + if provider.make_creating + WaitForClusterCreationWorker.perform_in( + Clusters::Aws::VerifyProvisionStatusService::INITIAL_INTERVAL, + provider.cluster_id + ) + else + provider.make_errored!("Failed to update provider record; #{provider.errors.full_messages}") + end + rescue Clusters::Aws::FetchCredentialsService::MissingRoleError + provider.make_errored!('Amazon role is not configured') + rescue ::Aws::Errors::MissingCredentialsError, Settingslogic::MissingSetting + provider.make_errored!('Amazon credentials are not configured') + rescue ::Aws::STS::Errors::ServiceError => e + provider.make_errored!("Amazon authentication failed; #{e.message}") + rescue ::Aws::CloudFormation::Errors::ServiceError => e + provider.make_errored!("Amazon CloudFormation request failed; #{e.message}") + end + + private + + def credentials + @credentials ||= Clusters::Aws::FetchCredentialsService.new(provider).execute + end + + def configure_provider_credentials + provider.update!( + access_key_id: credentials.access_key_id, + secret_access_key: credentials.secret_access_key, + session_token: credentials.session_token + ) + end + + def provision_cluster + provider.api_client.create_stack( + stack_name: provider.cluster.name, + template_body: stack_template, + parameters: parameters, + capabilities: ["CAPABILITY_IAM"] + ) + end + + def parameters + [ + parameter('ClusterName', provider.cluster.name), + parameter('ClusterRole', provider.role_arn), + parameter('ClusterControlPlaneSecurityGroup', provider.security_group_id), + parameter('VpcId', provider.vpc_id), + parameter('Subnets', provider.subnet_ids.join(',')), + parameter('NodeAutoScalingGroupDesiredCapacity', provider.num_nodes.to_s), + parameter('NodeInstanceType', provider.instance_type), + parameter('KeyName', provider.key_name) + ] + end + + def parameter(key, value) + { parameter_key: key, parameter_value: value } + end + + def stack_template + File.read(Rails.root.join('vendor', 'aws', 'cloudformation', 'eks_cluster.yaml')) + end + end + end +end diff --git a/app/services/clusters/aws/verify_provision_status_service.rb b/app/services/clusters/aws/verify_provision_status_service.rb new file mode 100644 index 00000000000..99532662bc4 --- /dev/null +++ b/app/services/clusters/aws/verify_provision_status_service.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Clusters + module Aws + class VerifyProvisionStatusService + attr_reader :provider + + INITIAL_INTERVAL = 5.minutes + POLL_INTERVAL = 1.minute + TIMEOUT = 30.minutes + + def execute(provider) + @provider = provider + + case cluster_stack.stack_status + when 'CREATE_IN_PROGRESS' + continue_creation + when 'CREATE_COMPLETE' + finalize_creation + else + provider.make_errored!("Unexpected status; #{cluster_stack.stack_status}") + end + rescue ::Aws::CloudFormation::Errors::ServiceError => e + provider.make_errored!("Amazon CloudFormation request failed; #{e.message}") + end + + private + + def cluster_stack + @cluster_stack ||= provider.api_client.describe_stacks(stack_name: provider.cluster.name).stacks.first + end + + def continue_creation + if timeout_threshold.future? + WaitForClusterCreationWorker.perform_in(POLL_INTERVAL, provider.cluster_id) + else + provider.make_errored!(_('Kubernetes cluster creation time exceeds timeout; %{timeout}') % { timeout: TIMEOUT }) + end + end + + def timeout_threshold + cluster_stack.creation_time + TIMEOUT + end + + def finalize_creation + Clusters::Aws::FinalizeCreationService.new.execute(provider) + end + end + end +end diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb index 375bcff4d3d..c34284319dd 100644 --- a/app/workers/cluster_provision_worker.rb +++ b/app/workers/cluster_provision_worker.rb @@ -9,7 +9,11 @@ class ClusterProvisionWorker def perform(cluster_id) Clusters::Cluster.find_by_id(cluster_id).try do |cluster| cluster.provider.try do |provider| - Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp? + if cluster.gcp? + Clusters::Gcp::ProvisionService.new.execute(provider) + elsif cluster.aws? + Clusters::Aws::ProvisionService.new.execute(provider) + end end end end diff --git a/app/workers/new_release_worker.rb b/app/workers/new_release_worker.rb index 28d2517238e..a3a882f9343 100644 --- a/app/workers/new_release_worker.rb +++ b/app/workers/new_release_worker.rb @@ -7,7 +7,7 @@ class NewReleaseWorker feature_category :release_orchestration def perform(release_id) - release = Release.with_project_and_namespace.find_by_id(release_id) + release = Release.preloaded.find_by_id(release_id) return unless release NotificationService.new.send_new_release_notifications(release) diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb index 770417398cb..621125c8503 100644 --- a/app/workers/wait_for_cluster_creation_worker.rb +++ b/app/workers/wait_for_cluster_creation_worker.rb @@ -9,7 +9,11 @@ class WaitForClusterCreationWorker def perform(cluster_id) Clusters::Cluster.find_by_id(cluster_id).try do |cluster| cluster.provider.try do |provider| - Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) if cluster.gcp? + if cluster.gcp? + Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) + elsif cluster.aws? + Clusters::Aws::VerifyProvisionStatusService.new.execute(provider) + end end end end |