diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-01 06:06:26 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-01 06:06:26 +0300 |
commit | 56d96ad7fab4d4b95f5529d8080b3cc2873794a0 (patch) | |
tree | 7fe93fc8ff4d82d815000781ffb9c98d7259211a /app/services | |
parent | 8078bd185fd9fce86cb5a8d9a6b6209e0c23ae44 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/services')
4 files changed, 326 insertions, 0 deletions
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 |