Commit 7ebc18d1 authored by Thong Kuah's avatar Thong Kuah

When provisioning a new cluster, create gitlab service account so that GitLab...

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.
parent fe450ebf
...@@ -8,18 +8,30 @@ module Clusters ...@@ -8,18 +8,30 @@ module Clusters
def execute(provider) def execute(provider)
@provider = provider @provider = provider
create_gitlab_service_account!
configure_provider configure_provider
configure_kubernetes configure_kubernetes
cluster.save! cluster.save!
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") 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 rescue ActiveRecord::RecordInvalid => e
provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}") provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}")
end end
private 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 def configure_provider
provider.endpoint = gke_cluster.endpoint provider.endpoint = gke_cluster.endpoint
provider.status_event = :make_created provider.status_event = :make_created
...@@ -32,6 +44,7 @@ module Clusters ...@@ -32,6 +44,7 @@ module Clusters
ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
username: gke_cluster.master_auth.username, username: gke_cluster.master_auth.username,
password: gke_cluster.master_auth.password, password: gke_cluster.master_auth.password,
authorization_type: authorization_type,
token: request_kubernetes_token) token: request_kubernetes_token)
end end
...@@ -43,6 +56,11 @@ module Clusters ...@@ -43,6 +56,11 @@ module Clusters
gke_cluster.master_auth.password).execute gke_cluster.master_auth.password).execute
end end
# GKE Clusters have RBAC enabled on Kubernetes >= 1.6
def authorization_type
'rbac'
end
def gke_cluster def gke_cluster
@gke_cluster ||= provider.api_client.projects_zones_clusters_get( @gke_cluster ||= provider.api_client.projects_zones_clusters_get(
provider.gcp_project_id, provider.gcp_project_id,
......
# 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
# 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
...@@ -16,7 +16,7 @@ module Clusters ...@@ -16,7 +16,7 @@ module Clusters
def execute def execute
read_secrets.each do |secret| read_secrets.each do |secret|
name = secret.dig('metadata', 'name') name = secret.dig('metadata', 'name')
if /default-token/ =~ name if token_regex =~ name
token_base64 = secret.dig('data', 'token') token_base64 = secret.dig('data', 'token')
return Base64.decode64(token_base64) if token_base64 return Base64.decode64(token_base64) if token_base64
end end
...@@ -27,6 +27,10 @@ module Clusters ...@@ -27,6 +27,10 @@ module Clusters
private private
def token_regex
/#{SERVICE_ACCOUNT_NAME}-token/
end
def read_secrets def read_secrets
kubeclient = build_kubeclient! kubeclient = build_kubeclient!
......
...@@ -45,6 +45,8 @@ describe Clusters::Gcp::FinalizeCreationService do ...@@ -45,6 +45,8 @@ describe Clusters::Gcp::FinalizeCreationService do
) )
stub_kubeclient_discover(api_url) stub_kubeclient_discover(api_url)
stub_kubeclient_create_service_account(api_url)
stub_kubeclient_create_cluster_role_binding(api_url)
end end
context 'when suceeded to fetch kuberenetes token' do context 'when suceeded to fetch kuberenetes token' do
...@@ -54,6 +56,7 @@ describe Clusters::Gcp::FinalizeCreationService do ...@@ -54,6 +56,7 @@ describe Clusters::Gcp::FinalizeCreationService do
stub_kubeclient_get_secrets( stub_kubeclient_get_secrets(
api_url, api_url,
{ {
metadata_name: 'gitlab-token-Y1a',
token: Base64.encode64(token) token: Base64.encode64(token)
} ) } )
end end
...@@ -71,6 +74,7 @@ describe Clusters::Gcp::FinalizeCreationService do ...@@ -71,6 +74,7 @@ describe Clusters::Gcp::FinalizeCreationService do
expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert))
expect(platform.username).to eq(username) expect(platform.username).to eq(username)
expect(platform.password).to eq(password) expect(platform.password).to eq(password)
expect(platform.authorization_type).to eq('rbac')
expect(platform.token).to eq(token) expect(platform.token).to eq(token)
end end
end end
......
# 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
...@@ -14,6 +14,14 @@ describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do ...@@ -14,6 +14,14 @@ describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do
let(:secrets_json) do let(:secrets_json) do
[ [
{
'metadata': {
name: 'default-token-123'
},
'data': {
'token': Base64.encode64('yyy.token.yyy')
}
},
{ {
'metadata': { 'metadata': {
name: metadata_name name: metadata_name
...@@ -30,13 +38,13 @@ describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do ...@@ -30,13 +38,13 @@ describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do
.to receive(:get_secrets).and_return(secrets_json) .to receive(:get_secrets).and_return(secrets_json)
end end
context 'when default-token exists' do context 'when gitlab-token exists' do
let(:metadata_name) { 'default-token-123' } let(:metadata_name) { 'gitlab-token-123' }
it { is_expected.to eq(token) } it { is_expected.to eq(token) }
end end
context 'when default-token does not exist' do context 'when gitlab-token does not exist' do
let(:metadata_name) { 'another-token-123' } let(:metadata_name) { 'another-token-123' }
it { is_expected.to be_nil } it { is_expected.to be_nil }
......
...@@ -43,6 +43,16 @@ module KubernetesHelpers ...@@ -43,6 +43,16 @@ module KubernetesHelpers
.to_return(status: [404, "Internal Server Error"]) .to_return(status: [404, "Internal Server Error"])
end 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) def kube_v1_secrets_body(**options)
{ {
"kind" => "SecretList", "kind" => "SecretList",
...@@ -68,6 +78,7 @@ module KubernetesHelpers ...@@ -68,6 +78,7 @@ module KubernetesHelpers
{ "name" => "pods", "namespaced" => true, "kind" => "Pod" }, { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
{ "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" },
{ "name" => "secrets", "namespaced" => true, "kind" => "Secret" }, { "name" => "secrets", "namespaced" => true, "kind" => "Secret" },
{ "name" => "serviceaccounts", "namespaced" => true, "kind" => "ServiceAccount" },
{ "name" => "services", "namespaced" => true, "kind" => "Service" } { "name" => "services", "namespaced" => true, "kind" => "Service" }
] ]
} }
...@@ -80,6 +91,7 @@ module KubernetesHelpers ...@@ -80,6 +91,7 @@ module KubernetesHelpers
{ "name" => "pods", "namespaced" => true, "kind" => "Pod" }, { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
{ "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" },
{ "name" => "secrets", "namespaced" => true, "kind" => "Secret" }, { "name" => "secrets", "namespaced" => true, "kind" => "Secret" },
{ "name" => "serviceaccounts", "namespaced" => true, "kind" => "ServiceAccount" },
{ "name" => "services", "namespaced" => true, "kind" => "Service" } { "name" => "services", "namespaced" => true, "kind" => "Service" }
] ]
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment