Read about our upcoming Code of Conduct on this issue

Commit b8820f95 authored by Thong Kuah's avatar Thong Kuah Committed by Kamil Trzciński
Browse files

Incorporates Kubernetes Namespace into Cluster's flow

parent 85929427548d
......@@ -19,6 +19,7 @@ class Cluster < ActiveRecord::Base
has_many :cluster_projects, class_name: 'Clusters::Project'
has_many :projects, through: :cluster_projects, class_name: '::Project'
has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project'
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :groups, through: :cluster_groups, class_name: '::Group'
......@@ -128,6 +129,13 @@ def kubeclient
platform_kubernetes.kubeclient if kubernetes?
end
def find_or_initialize_kubernetes_namespace(cluster_project)
kubernetes_namespaces.find_or_initialize_by(
project: cluster_project.project,
cluster_project: cluster_project
)
end
private
def restrict_modification
......
......@@ -2,6 +2,8 @@
module Clusters
class KubernetesNamespace < ActiveRecord::Base
include Gitlab::Kubernetes
self.table_name = 'clusters_kubernetes_namespaces'
belongs_to :cluster_project, class_name: 'Clusters::Project'
......@@ -12,7 +14,8 @@ class KubernetesNamespace < ActiveRecord::Base
validates :namespace, presence: true
validates :namespace, uniqueness: { scope: :cluster_id }
before_validation :set_namespace_and_service_account_to_default, on: :create
delegate :ca_pem, to: :platform_kubernetes, allow_nil: true
delegate :api_url, to: :platform_kubernetes, allow_nil: true
attr_encrypted :service_account_token,
mode: :per_attribute_iv,
......@@ -23,14 +26,26 @@ def token_name
"#{namespace}-token"
end
private
def configure_predefined_credentials
self.namespace = kubernetes_or_project_namespace
self.service_account_name = default_service_account_name
end
def predefined_variables
config = YAML.dump(kubeconfig)
def set_namespace_and_service_account_to_default
self.namespace ||= default_namespace
self.service_account_name ||= default_service_account_name
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables
.append(key: 'KUBE_SERVICE_ACCOUNT', value: service_account_name)
.append(key: 'KUBE_NAMESPACE', value: namespace)
.append(key: 'KUBE_TOKEN', value: service_account_token, public: false)
.append(key: 'KUBECONFIG', value: config, public: false, file: true)
end
end
def default_namespace
private
def kubernetes_or_project_namespace
platform_kubernetes&.namespace.presence || project_namespace
end
......@@ -45,5 +60,13 @@ def project_namespace
def project_slug
"#{project.path}-#{project.id}".downcase
end
def kubeconfig
to_kubeconfig(
url: api_url,
namespace: namespace,
token: service_account_token,
ca_pem: ca_pem)
end
end
end
......@@ -6,6 +6,7 @@ class Kubernetes < ActiveRecord::Base
include Gitlab::Kubernetes
include ReactiveCaching
include EnumWithNil
include AfterCommitQueue
RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
......@@ -43,6 +44,7 @@ class Kubernetes < ActiveRecord::Base
validate :prevent_modification, on: :update
after_save :clear_reactive_cache!
after_update :update_kubernetes_namespace
alias_attribute :ca_pem, :ca_cert
......@@ -67,21 +69,31 @@ def actual_namespace
end
end
def predefined_variables
config = YAML.dump(kubeconfig)
def predefined_variables(project:)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables
.append(key: 'KUBE_URL', value: api_url)
.append(key: 'KUBE_TOKEN', value: token, public: false)
.append(key: 'KUBE_NAMESPACE', value: actual_namespace)
.append(key: 'KUBECONFIG', value: config, public: false, file: true)
variables.append(key: 'KUBE_URL', value: api_url)
if ca_pem.present?
variables
.append(key: 'KUBE_CA_PEM', value: ca_pem)
.append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true)
end
if kubernetes_namespace = cluster.kubernetes_namespaces.find_by(project: project)
variables.concat(kubernetes_namespace.predefined_variables)
else
# From 11.5, every Clusters::Project should have at least one
# Clusters::KubernetesNamespace, so once migration has been completed,
# this 'else' branch will be removed. For more information, please see
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22433
config = YAML.dump(kubeconfig)
variables
.append(key: 'KUBE_URL', value: api_url)
.append(key: 'KUBE_TOKEN', value: token, public: false)
.append(key: 'KUBE_NAMESPACE', value: actual_namespace)
.append(key: 'KUBECONFIG', value: config, public: false, file: true)
end
end
end
......@@ -199,6 +211,14 @@ def prevent_modification
true
end
def update_kubernetes_namespace
return unless namespace_changed?
run_after_commit do
ClusterPlatformConfigureWorker.perform_async(cluster_id)
end
end
end
end
end
......@@ -1829,7 +1829,7 @@ def protected_for?(ref)
end
def deployment_variables(environment: nil)
deployment_platform(environment: environment)&.predefined_variables || []
deployment_platform(environment: environment)&.predefined_variables(project: self) || []
end
def auto_devops_variables
......
......@@ -104,7 +104,12 @@ def test(*args)
{ success: false, result: err }
end
def predefined_variables
# Project param was added on
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011,
# as a way to keep this service compatible with
# Clusters::Platforms::Kubernetes, it won't be used on this method
# as it's only needed for Clusters::Cluster.
def predefined_variables(project:)
config = YAML.dump(kubeconfig)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
......
......@@ -11,8 +11,9 @@ def execute(provider)
configure_provider
create_gitlab_service_account!
configure_kubernetes
cluster.save!
configure_project_service_account
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
......@@ -24,7 +25,10 @@ def execute(provider)
private
def create_gitlab_service_account!
Clusters::Gcp::Kubernetes::CreateServiceAccountService.new(kube_client, rbac: create_rbac_cluster?).execute
Clusters::Gcp::Kubernetes::CreateServiceAccountService.gitlab_creator(
kube_client,
rbac: create_rbac_cluster?
).execute
end
def configure_provider
......@@ -44,7 +48,20 @@ def configure_kubernetes
end
def request_kubernetes_token
Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(kube_client).execute
Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(
kube_client,
Clusters::Gcp::Kubernetes::GITLAB_ADMIN_TOKEN_NAME,
Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE
).execute
end
def configure_project_service_account
kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project)
Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
cluster: cluster,
kubernetes_namespace: kubernetes_namespace
).execute
end
def authorization_type
......
......@@ -3,11 +3,12 @@
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'
GITLAB_SERVICE_ACCOUNT_NAME = 'gitlab'
GITLAB_SERVICE_ACCOUNT_NAMESPACE = 'default'
GITLAB_ADMIN_TOKEN_NAME = 'gitlab-token'
GITLAB_CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin'
GITLAB_CLUSTER_ROLE_NAME = 'cluster-admin'
PROJECT_CLUSTER_ROLE_NAME = 'edit'
end
end
end
# frozen_string_literal: true
module Clusters
module Gcp
module Kubernetes
class CreateOrUpdateNamespaceService
def initialize(cluster:, kubernetes_namespace:)
@cluster = cluster
@kubernetes_namespace = kubernetes_namespace
@platform = cluster.platform
end
def execute
configure_kubernetes_namespace
create_project_service_account
configure_kubernetes_token
kubernetes_namespace.save!
rescue ::Kubeclient::HttpError => err
raise err unless err.error_code = 404
end
private
attr_reader :cluster, :kubernetes_namespace, :platform
def configure_kubernetes_namespace
kubernetes_namespace.configure_predefined_credentials
end
def create_project_service_account
Clusters::Gcp::Kubernetes::CreateServiceAccountService.namespace_creator(
platform.kubeclient,
service_account_name: kubernetes_namespace.service_account_name,
service_account_namespace: kubernetes_namespace.namespace,
rbac: platform.rbac?
).execute
end
def configure_kubernetes_token
kubernetes_namespace.service_account_token = fetch_service_account_token
end
def fetch_service_account_token
Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(
platform.kubeclient,
kubernetes_namespace.token_name,
kubernetes_namespace.namespace
).execute
end
end
end
end
end
......@@ -4,46 +4,96 @@ module Clusters
module Gcp
module Kubernetes
class CreateServiceAccountService
attr_reader :kubeclient, :rbac
def initialize(kubeclient, rbac:)
def initialize(kubeclient, service_account_name:, service_account_namespace:, token_name:, rbac:, namespace_creator: false, role_binding_name: nil)
@kubeclient = kubeclient
@service_account_name = service_account_name
@service_account_namespace = service_account_namespace
@token_name = token_name
@rbac = rbac
@namespace_creator = namespace_creator
@role_binding_name = role_binding_name
end
def self.gitlab_creator(kubeclient, rbac:)
self.new(
kubeclient,
service_account_name: Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAME,
service_account_namespace: Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE,
token_name: Clusters::Gcp::Kubernetes::GITLAB_ADMIN_TOKEN_NAME,
rbac: rbac
)
end
def self.namespace_creator(kubeclient, service_account_name:, service_account_namespace:, rbac:)
self.new(
kubeclient,
service_account_name: service_account_name,
service_account_namespace: service_account_namespace,
token_name: "#{service_account_namespace}-token",
rbac: rbac,
namespace_creator: true,
role_binding_name: "gitlab-#{service_account_namespace}"
)
end
def execute
ensure_project_namespace_exists if namespace_creator
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
create_role_or_cluster_role_binding if rbac
end
private
attr_reader :kubeclient, :service_account_name, :service_account_namespace, :token_name, :rbac, :namespace_creator, :role_binding_name
def ensure_project_namespace_exists
Gitlab::Kubernetes::Namespace.new(
service_account_namespace,
kubeclient
).ensure_exists!
end
def create_role_or_cluster_role_binding
if namespace_creator
kubeclient.create_role_binding(role_binding_resource)
else
kubeclient.create_cluster_role_binding(cluster_role_binding_resource)
end
end
def service_account_resource
Gitlab::Kubernetes::ServiceAccount.new(service_account_name, service_account_namespace).generate
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
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,
Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_BINDING_NAME,
Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_NAME,
subjects
).generate
end
def service_account_name
SERVICE_ACCOUNT_NAME
end
def service_account_namespace
SERVICE_ACCOUNT_NAMESPACE
def role_binding_resource
Gitlab::Kubernetes::RoleBinding.new(
name: role_binding_name,
role_name: Clusters::Gcp::Kubernetes::PROJECT_CLUSTER_ROLE_NAME,
namespace: service_account_namespace,
service_account_name: service_account_name
).generate
end
end
end
......
......@@ -4,10 +4,12 @@ module Clusters
module Gcp
module Kubernetes
class FetchKubernetesTokenService
attr_reader :kubeclient
attr_reader :kubeclient, :service_account_token_name, :namespace
def initialize(kubeclient)
def initialize(kubeclient, service_account_token_name, namespace)
@kubeclient = kubeclient
@service_account_token_name = service_account_token_name
@namespace = namespace
end
def execute
......@@ -18,7 +20,7 @@ def execute
private
def get_secret
kubeclient.get_secret(SERVICE_ACCOUNT_TOKEN_NAME, SERVICE_ACCOUNT_NAMESPACE).as_json
kubeclient.get_secret(service_account_token_name, namespace).as_json
rescue Kubeclient::HttpError => err
raise err unless err.error_code == 404
......
......@@ -28,6 +28,7 @@
- gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation
- gcp_cluster:cluster_wait_for_ingress_ip_address
- gcp_cluster:cluster_platform_configure
- github_import_advance_stage
- github_importer:github_import_import_diff_note
......
# frozen_string_literal: true
class ClusterPlatformConfigureWorker
include ApplicationWorker
include ClusterQueue
def perform(cluster_id)
Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
next unless cluster.cluster_project
kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project)
Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
cluster: cluster,
kubernetes_namespace: kubernetes_namespace
).execute
end
rescue ::Kubeclient::HttpError => err
Rails.logger.error "Failed to create/update Kubernetes Namespace. id: #{kubernetes_namespace.id} message: #{err.message}"
end
end
......@@ -9,6 +9,8 @@ def perform(cluster_id)
cluster.provider.try do |provider|
Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp?
end
ClusterPlatformConfigureWorker.perform_async(cluster.id) if cluster.user?
end
end
end
---
title: Extend RBAC by having a service account restricted to project's namespace
merge_request: 22011
author:
type: other
......@@ -3,9 +3,8 @@
module Gitlab
module Kubernetes
class RoleBinding
attr_reader :role_name, :namespace, :service_account_name
def initialize(role_name:, namespace:, service_account_name:)
def initialize(name:, role_name:, namespace:, service_account_name:)
@name = name
@role_name = role_name
@namespace = namespace
@service_account_name = service_account_name
......@@ -21,14 +20,16 @@ def generate
private
attr_reader :name, :role_name, :namespace, :service_account_name
def metadata
{ name: "gitlab-#{namespace}", namespace: namespace }
{ name: name, namespace: namespace }
end
def role_ref
{
apiGroup: 'rbac.authorization.k8s.io',
kind: 'Role',
kind: 'ClusterRole',
name: role_name
}
end
......
......@@ -5,6 +5,7 @@
describe Projects::ClustersController do
include AccessMatchersForController
include GoogleApi::CloudPlatformHelpers
include KubernetesHelpers
set(:project) { create(:project) }
......@@ -309,6 +310,11 @@ def go
end
describe 'security' do
before do
allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
end
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:maintainer).of(project) }
......@@ -412,6 +418,11 @@ def go(format: :html)
)
end
before do
allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
end
context 'when cluster is provided by GCP' do
it "updates and redirects back to show page" do
go
......
......@@ -2,8 +2,18 @@
FactoryBot.define do
factory :cluster_kubernetes_namespace, class: Clusters::KubernetesNamespace do
cluster
project
cluster_project
association :cluster, :project, :provided_by_gcp
namespace { |n| "environment#{n}" }
after(:build) do |kubernetes_namespace|
cluster_project = kubernetes_namespace.cluster.cluster_project
kubernetes_namespace.project = cluster_project.project
kubernetes_namespace.cluster_project = cluster_project
end
trait :with_token do
service_account_token { Faker::Lorem.characters(10) }
end
end
end
......@@ -130,6 +130,7 @@
context 'when user changes cluster parameters' do
before do
allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
page.within('#js-cluster-details') { click_button 'Save changes' }
end
......
......@@ -9,7 +9,9 @@
before do
project.add_maintainer(user)
gitlab_sign_in(user)
allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
end
context 'when user does not have a cluster and visits cluster index page' do
......
......@@ -20,7 +20,7 @@
let(:role_ref) do
{
apiGroup: 'rbac.authorization.k8s.io',
kind: 'Role',
kind: 'ClusterRole',
name: role_name
}
end
......@@ -35,6 +35,7 @@
subject do
described_class.new(