diff options
author | Kamil Trzciński <ayufan@ayufan.eu> | 2018-09-17 11:43:48 +0300 |
---|---|---|
committer | Kamil Trzciński <ayufan@ayufan.eu> | 2018-09-17 11:43:48 +0300 |
commit | 5a8908bf587a0723b07e510dd6118a686d49af98 (patch) | |
tree | a19af868a1b0a7c676b56a43372811a662754b6f /app | |
parent | ba40c7f1c32f62ad5370621f049500aa904149cd (diff) | |
parent | 528b060b89c2d6a6be611e88ceed28cfe86e167c (diff) |
Merge branch '29398-support-rbac-for-gitlab-provisioned-clusters' into 'master'
Support Kubernetes RBAC for GitLab Managed Apps for creating new clusters
Closes #29398
See merge request gitlab-org/gitlab-ce!21401
Diffstat (limited to 'app')
10 files changed, 167 insertions, 79 deletions
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index b4fd09c06e5..eb0fad6cbb2 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -141,7 +141,8 @@ class Projects::ClustersController < Projects::ApplicationController :gcp_project_id, :zone, :num_nodes, - :machine_type + :machine_type, + :legacy_abac ]).merge( provider_type: :gcp, platform_type: :kubernetes diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb deleted file mode 100644 index 15eda56cac6..00000000000 --- a/app/services/ci/fetch_kubernetes_token_service.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -## -# TODO: -# Almost components in this class were copied from app/models/project_services/kubernetes_service.rb -# We should dry up those classes not to repeat the same code. -# Maybe we should have a special facility (e.g. lib/kubernetes_api) to maintain all Kubernetes API caller. -module Ci - class FetchKubernetesTokenService - attr_reader :api_url, :ca_pem, :username, :password - - def initialize(api_url, ca_pem, username, password) - @api_url = api_url - @ca_pem = ca_pem - @username = username - @password = password - end - - def execute - read_secrets.each do |secret| - name = secret.dig('metadata', 'name') - if /default-token/ =~ name - token_base64 = secret.dig('data', 'token') - return Base64.decode64(token_base64) if token_base64 - end - end - - nil - end - - private - - def read_secrets - kubeclient = build_kubeclient! - - kubeclient.get_secrets.as_json - rescue Kubeclient::HttpError => err - raise err unless err.error_code == 404 - - [] - end - - def build_kubeclient!(api_path: 'api', api_version: 'v1') - raise "Incomplete settings" unless api_url && username && password - - ::Kubeclient::Client.new( - join_api_url(api_path), - api_version, - auth_options: { username: username, password: password }, - ssl_options: kubeclient_ssl_options, - http_proxy_uri: ENV['http_proxy'] - ) - end - - def join_api_url(api_path) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [prefix, api_path].join("/") - - url.to_s - end - - def kubeclient_ssl_options - 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 - end -end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 264419501dc..3ae0a4a19d0 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -9,17 +9,24 @@ module Clusters @provider = provider configure_provider + create_gitlab_service_account! configure_kubernetes cluster.save! rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") + rescue Kubeclient::HttpError => e + provider.make_errored!("Failed to run Kubeclient: #{e.message}") rescue ActiveRecord::RecordInvalid => e provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}") end private + def create_gitlab_service_account! + Clusters::Gcp::Kubernetes::CreateServiceAccountService.new(kube_client, rbac: create_rbac_cluster?).execute + end + def configure_provider provider.endpoint = gke_cluster.endpoint provider.status_event = :make_created @@ -32,15 +39,54 @@ module Clusters ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), username: gke_cluster.master_auth.username, password: gke_cluster.master_auth.password, + authorization_type: authorization_type, token: request_kubernetes_token) end def request_kubernetes_token - Ci::FetchKubernetesTokenService.new( + Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(kube_client).execute + end + + def authorization_type + create_rbac_cluster? ? 'rbac' : 'abac' + end + + def create_rbac_cluster? + !provider.legacy_abac? + end + + def kube_client + @kube_client ||= build_kube_client!( 'https://' + gke_cluster.endpoint, Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), gke_cluster.master_auth.username, - gke_cluster.master_auth.password).execute + gke_cluster.master_auth.password, + api_groups: ['api', 'apis/rbac.authorization.k8s.io'] + ) + end + + def build_kube_client!(api_url, ca_pem, username, password, api_groups: ['api'], api_version: 'v1') + raise "Incomplete settings" unless api_url && username && password + + Gitlab::Kubernetes::KubeClient.new( + api_url, + api_groups, + api_version, + auth_options: { username: username, password: password }, + ssl_options: kubeclient_ssl_options(ca_pem), + http_proxy_uri: ENV['http_proxy'] + ) + 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 gke_cluster diff --git a/app/services/clusters/gcp/kubernetes.rb b/app/services/clusters/gcp/kubernetes.rb new file mode 100644 index 00000000000..d014d73b3e8 --- /dev/null +++ b/app/services/clusters/gcp/kubernetes.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + SERVICE_ACCOUNT_NAME = 'gitlab' + SERVICE_ACCOUNT_NAMESPACE = 'default' + SERVICE_ACCOUNT_TOKEN_NAME = 'gitlab-token' + CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin' + CLUSTER_ROLE_NAME = 'cluster-admin' + end + end +end diff --git a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb new file mode 100644 index 00000000000..d17744591e6 --- /dev/null +++ b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + class CreateServiceAccountService + attr_reader :kubeclient, :rbac + + def initialize(kubeclient, rbac:) + @kubeclient = kubeclient + @rbac = rbac + end + + def execute + kubeclient.create_service_account(service_account_resource) + kubeclient.create_secret(service_account_token_resource) + kubeclient.create_cluster_role_binding(cluster_role_binding_resource) if rbac + end + + private + + def service_account_resource + Gitlab::Kubernetes::ServiceAccount.new(service_account_name, service_account_namespace).generate + end + + def service_account_token_resource + Gitlab::Kubernetes::ServiceAccountToken.new( + SERVICE_ACCOUNT_TOKEN_NAME, service_account_name, service_account_namespace).generate + end + + def cluster_role_binding_resource + subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }] + + Gitlab::Kubernetes::ClusterRoleBinding.new( + CLUSTER_ROLE_BINDING_NAME, + CLUSTER_ROLE_NAME, + subjects + ).generate + end + + def service_account_name + SERVICE_ACCOUNT_NAME + end + + def service_account_namespace + SERVICE_ACCOUNT_NAMESPACE + end + end + end + end +end diff --git a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb new file mode 100644 index 00000000000..9e09345c8dc --- /dev/null +++ b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + class FetchKubernetesTokenService + attr_reader :kubeclient + + def initialize(kubeclient) + @kubeclient = kubeclient + end + + def execute + token_base64 = get_secret&.dig('data', 'token') + Base64.decode64(token_base64) if token_base64 + end + + private + + def get_secret + kubeclient.get_secret(SERVICE_ACCOUNT_TOKEN_NAME, SERVICE_ACCOUNT_NAMESPACE).as_json + rescue Kubeclient::HttpError => err + raise err unless err.error_code == 404 + + nil + end + end + end + end +end diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb index ab1bf9c64f6..80040511ec2 100644 --- a/app/services/clusters/gcp/provision_service.rb +++ b/app/services/clusters/gcp/provision_service.rb @@ -27,7 +27,9 @@ module Clusters provider.zone, provider.cluster.name, provider.num_nodes, - machine_type: provider.machine_type) + machine_type: provider.machine_type, + legacy_abac: provider.legacy_abac + ) unless operation.status == 'PENDING' || operation.status == 'RUNNING' return provider.make_errored!("Operation status is unexpected; #{operation.status_message}") diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index 9133de6559d..0222bbf7338 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -61,5 +61,15 @@ %p.form-text.text-muted = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } + - if rbac_clusters_feature_enabled? + .form-group + .form-check + = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true + = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank' + .form-group = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml index 877e0cc876c..be84f2ae67c 100644 --- a/app/views/projects/clusters/gcp/_show.html.haml +++ b/app/views/projects/clusters/gcp/_show.html.haml @@ -37,5 +37,14 @@ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + - if rbac_clusters_feature_enabled? + .form-group + .form-check + = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + .form-group = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index 1f81e024ab9..f497f5b606c 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -33,6 +33,7 @@ .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank' .form-group = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success' |