summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThong Kuah <tkuah@gitlab.com>2018-08-29 21:07:01 +1200
committerThong Kuah <tkuah@gitlab.com>2018-09-14 16:26:50 +1200
commit7ebc18d1b3d398e3635feec1939ee3dac6c4a2a0 (patch)
tree860e8425064c1b20e889555f1d4c05e117e93242
parentfe450ebf51abd9fa96a0eff01ad074fc4cfbedab (diff)
downloadgitlab-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.
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb18
-rw-r--r--app/services/clusters/gcp/kubernetes.rb11
-rw-r--r--app/services/clusters/gcp/kubernetes/create_service_account_service.rb65
-rw-r--r--app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb6
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb4
-rw-r--r--spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb66
-rw-r--r--spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb14
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb12
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" }
]
}