diff options
author | Thong Kuah <tkuah@gitlab.com> | 2018-08-29 21:07:01 +1200 |
---|---|---|
committer | Thong Kuah <tkuah@gitlab.com> | 2018-09-14 16:26:50 +1200 |
commit | 7ebc18d1b3d398e3635feec1939ee3dac6c4a2a0 (patch) | |
tree | 860e8425064c1b20e889555f1d4c05e117e93242 | |
parent | fe450ebf51abd9fa96a0eff01ad074fc4cfbedab (diff) | |
download | gitlab-ce-7ebc18d1b3d398e3635feec1939ee3dac6c4a2a0.tar.gz |
When provisioning a new cluster, create gitlab service account so that GitLab can perform operations in a RBAC-enabled cluster.
Correspondingly, use the token of the gitlab service account, vs the
default service account token which will have no privs.
8 files changed, 192 insertions, 4 deletions
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 76b1f439569..29948b32192 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -8,18 +8,30 @@ module Clusters def execute(provider) @provider = provider + create_gitlab_service_account! + configure_provider 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( + 'https://' + gke_cluster.endpoint, + Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), + gke_cluster.master_auth.username, + gke_cluster.master_auth.password).execute + end + def configure_provider provider.endpoint = gke_cluster.endpoint provider.status_event = :make_created @@ -32,6 +44,7 @@ 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 @@ -43,6 +56,11 @@ module Clusters gke_cluster.master_auth.password).execute end + # GKE Clusters have RBAC enabled on Kubernetes >= 1.6 + def authorization_type + 'rbac' + end + def gke_cluster @gke_cluster ||= provider.api_client.projects_zones_clusters_get( provider.gcp_project_id, diff --git a/app/services/clusters/gcp/kubernetes.rb b/app/services/clusters/gcp/kubernetes.rb new file mode 100644 index 00000000000..74ef68eb58f --- /dev/null +++ b/app/services/clusters/gcp/kubernetes.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + SERVICE_ACCOUNT_NAME = 'gitlab' + 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..a9088578c81 --- /dev/null +++ b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + class CreateServiceAccountService + 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 + kubeclient = build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) + + kubeclient.create_service_account(service_account_resource) + kubeclient.create_cluster_role_binding(cluster_role_binding_resource) + end + + private + + def service_account_resource + Gitlab::Kubernetes::ServiceAccount.new(SERVICE_ACCOUNT_NAME, 'default').generate + end + + def cluster_role_binding_resource + subjects = [{ kind: 'ServiceAccount', name: SERVICE_ACCOUNT_NAME, namespace: 'default' }] + + Gitlab::Kubernetes::ClusterRoleBinding.new( + CLUSTER_ROLE_BINDING_NAME, + CLUSTER_ROLE_NAME, + subjects + ).generate + end + + def build_kube_client!(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, + http_proxy_uri: ENV['http_proxy'] + ) + 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 + 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 index 07c8eaae5d3..ba5e0ed9881 100644 --- a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb +++ b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb @@ -16,7 +16,7 @@ module Clusters def execute read_secrets.each do |secret| name = secret.dig('metadata', 'name') - if /default-token/ =~ name + if token_regex =~ name token_base64 = secret.dig('data', 'token') return Base64.decode64(token_base64) if token_base64 end @@ -27,6 +27,10 @@ module Clusters private + def token_regex + /#{SERVICE_ACCOUNT_NAME}-token/ + end + def read_secrets kubeclient = build_kubeclient! diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb index 0cf91307589..aec865872a0 100644 --- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb +++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb @@ -45,6 +45,8 @@ describe Clusters::Gcp::FinalizeCreationService do ) stub_kubeclient_discover(api_url) + stub_kubeclient_create_service_account(api_url) + stub_kubeclient_create_cluster_role_binding(api_url) end context 'when suceeded to fetch kuberenetes token' do @@ -54,6 +56,7 @@ describe Clusters::Gcp::FinalizeCreationService do stub_kubeclient_get_secrets( api_url, { + metadata_name: 'gitlab-token-Y1a', token: Base64.encode64(token) } ) end @@ -71,6 +74,7 @@ describe Clusters::Gcp::FinalizeCreationService do expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) expect(platform.username).to eq(username) expect(platform.password).to eq(password) + expect(platform.authorization_type).to eq('rbac') expect(platform.token).to eq(token) end end diff --git a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb new file mode 100644 index 00000000000..190f8395ff7 --- /dev/null +++ b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do + include KubernetesHelpers + + let(:service) { described_class.new(api_url, ca_pem, username, password) } + + describe '#execute' do + subject { service.execute } + + let(:api_url) { 'http://111.111.111.111' } + let(:ca_pem) { '' } + let(:username) { 'admin' } + let(:password) { 'xxx' } + + context 'when params are correct' do + before do + stub_kubeclient_discover(api_url) + stub_kubeclient_create_service_account(api_url) + stub_kubeclient_create_cluster_role_binding(api_url) + end + + it 'creates a kubernetes service account' do + subject + + expect(WebMock).to have_requested(:post, api_url + '/api/v1/namespaces/default/serviceaccounts').with( + body: hash_including( + metadata: { name: 'gitlab', namespace: 'default' } + ) + ) + end + + it 'creates a kubernetes cluster role binding' do + subject + + expect(WebMock).to have_requested(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings').with( + body: hash_including( + metadata: { name: 'gitlab-admin' }, + roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' }, + subjects: [{ kind: 'ServiceAccount', namespace: 'default', name: 'gitlab' }] + ) + ) + end + end + + context 'when api_url is nil' do + let(:api_url) { nil } + + it { expect { subject }.to raise_error("Incomplete settings") } + end + + context 'when username is nil' do + let(:username) { nil } + + it { expect { subject }.to raise_error("Incomplete settings") } + end + + context 'when password is nil' do + let(:password) { nil } + + it { expect { subject }.to raise_error("Incomplete settings") } + end + end +end diff --git a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb index 80fde84d299..30431557046 100644 --- a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb +++ b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb @@ -16,6 +16,14 @@ describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do [ { 'metadata': { + name: 'default-token-123' + }, + 'data': { + 'token': Base64.encode64('yyy.token.yyy') + } + }, + { + 'metadata': { name: metadata_name }, 'data': { @@ -30,13 +38,13 @@ describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do .to receive(:get_secrets).and_return(secrets_json) end - context 'when default-token exists' do - let(:metadata_name) { 'default-token-123' } + context 'when gitlab-token exists' do + let(:metadata_name) { 'gitlab-token-123' } it { is_expected.to eq(token) } end - context 'when default-token does not exist' do + context 'when gitlab-token does not exist' do let(:metadata_name) { 'another-token-123' } it { is_expected.to be_nil } diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 994a2aaef90..30af1e7928c 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -43,6 +43,16 @@ module KubernetesHelpers .to_return(status: [404, "Internal Server Error"]) end + def stub_kubeclient_create_service_account(api_url, namespace: 'default') + WebMock.stub_request(:post, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts") + .to_return(kube_response({})) + end + + def stub_kubeclient_create_cluster_role_binding(api_url) + WebMock.stub_request(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings') + .to_return(kube_response({})) + end + def kube_v1_secrets_body(**options) { "kind" => "SecretList", @@ -68,6 +78,7 @@ module KubernetesHelpers { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }, + { "name" => "serviceaccounts", "namespaced" => true, "kind" => "ServiceAccount" }, { "name" => "services", "namespaced" => true, "kind" => "Service" } ] } @@ -80,6 +91,7 @@ module KubernetesHelpers { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }, + { "name" => "serviceaccounts", "namespaced" => true, "kind" => "ServiceAccount" }, { "name" => "services", "namespaced" => true, "kind" => "Service" } ] } |