Commit 5a8908bf authored by Kamil Trzciński's avatar Kamil Trzciński

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
parents ba40c7f1 528b060b
......@@ -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
......
# 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
......@@ -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
......
# 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
# 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
# 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
......@@ -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}")
......
......@@ -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
......@@ -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'
......@@ -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'
---
title: Support Kubernetes RBAC for GitLab Managed Apps when creating new clusters
merge_request: 21401
author:
type: changed
# frozen_string_literal: true
class AddLegacyAbacToClusterProvidersGcp < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:cluster_providers_gcp, :legacy_abac, :boolean, default: true)
end
def down
remove_column(:cluster_providers_gcp, :legacy_abac)
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180906101639) do
ActiveRecord::Schema.define(version: 20180907015926) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -620,6 +620,7 @@ ActiveRecord::Schema.define(version: 20180906101639) do
t.string "endpoint"
t.text "encrypted_access_token"
t.string "encrypted_access_token_iv"
t.boolean "legacy_abac", default: true, null: false
end
add_index "cluster_providers_gcp", ["cluster_id"], name: "index_cluster_providers_gcp_on_cluster_id", unique: true, using: :btree
......
......@@ -127,8 +127,81 @@ applications running on the cluster.
When GitLab creates the cluster, it enables and uses the legacy
[Attribute-based access control (ABAC)](https://kubernetes.io/docs/admin/authorization/abac/).
The newer [RBAC](https://kubernetes.io/docs/admin/authorization/rbac/)
authorization will be supported in a
[future release](https://gitlab.com/gitlab-org/gitlab-ce/issues/29398).
authorization is [experimental](#role-based-access-control-rbac).
### Role-based access control (RBAC) **[CORE ONLY]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21401) in GitLab 11.4.
CAUTION: **Warning:**
The RBAC authorization is experimental. To enable it you need access to the
server where GitLab is installed.
The support for RBAC-enabled clusters is hidden behind a feature flag. Once
the feature flag is enabled, GitLab will create the necessary service accounts
and privileges in order to install and run [GitLab managed applications](#installing-applications).
To enable the feature flag:
1. SSH into the server where GitLab is installed.
1. Enter the Rails console:
**For Omnibus GitLab**
```sh
sudo gitlab-rails console
```
**For installations from source**
```sh
sudo -u git -H bundle exec rails console
```
1. Enable the RBAC authorization:
```ruby
Feature.enable('rbac_clusters')
```
If you are creating a [new GKE cluster via
GitLab](#adding-and-creating-a-new-gke-cluster-via-gitlab), you will be
asked if you would like to create an RBAC-enabled cluster. Enabling this
setting will create a `gitlab` service account which will be used by
GitLab to manage the newly created cluster. To enable this, this service
account will have the `cluster-admin` privilege.
If you are [adding an existing Kubernetes
cluster](#adding-an-existing-kubernetes-cluster), you will be asked if
the cluster you are adding is a RBAC-enabled cluster. Ensure the
token of the account has administrator privileges for the cluster.
In both cases above, when you install Helm Tiller into your cluster, an
RBAC-enabled cluster will create a `tiller` service account, with `cluster-admin`
privileges in the `gitlab-managed-apps` namespace. This service account will be
added to the installed Helm Tiller and will be used by Helm to install and run
[GitLab managed applications](#installing-applications).
The table below summarizes which resources will be created in a
RBAC-enabled cluster :
| Name | Kind | Details | Created when |
| --- | --- | --- | --- |
| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
| `gitlab-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Creating a new GKE Cluster |
| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
Helm Tiller will also create additional service accounts and other RBAC
resources for each installed application. Consult the documentation for the
Helm charts for each application for details.
NOTE: **Note:**
Auto DevOps will not successfully complete in a cluster that only has RBAC
authorization enabled. RBAC support for Auto DevOps is planned in a
[future release](https://gitlab.com/gitlab-org/gitlab-ce/issues/44597).
### Security of GitLab Runners
......@@ -161,13 +234,13 @@ with Tiller already installed, you should be careful as GitLab cannot
detect it. By installing it via the applications will result into having it
twice, which can lead to confusion during deployments.
| Application | GitLab version | Description |
| ----------- | :------------: | ----------- |
| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. |
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. |
| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. |
| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. We use [this](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile) custom Jupyter image that installs additional useful packages on top of the base Jupyter. **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. |
| Application | GitLab version | Description | Helm Chart |
| ----------- | :------------: | ----------- | --------------- |
| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | n/a |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. | [stable/nginx-ingress](https://github.com/helm/charts/tree/master/stable/nginx-ingress) |
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. | [stable/prometheus](https://github.com/helm/charts/tree/master/stable/prometheus) |
| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | [runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) |
| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. We use [this](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile) custom Jupyter image that installs additional useful packages on top of the base Jupyter. **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. | [jupyter/jupyterhub](https://jupyterhub.github.io/helm-chart/) |
## Getting the external IP address
......
......@@ -25,12 +25,14 @@ module Gitlab
:get_config_map,
:get_namespace,
:get_pod,
:get_secret,
:get_service,
:get_service_account,
:delete_pod,
:create_config_map,
:create_namespace,
:create_pod,
:create_secret,
:create_service_account,
:update_config_map,
:update_service_account,
......
# frozen_string_literal: true
module Gitlab
module Kubernetes
class ServiceAccountToken
attr_reader :name, :service_account_name, :namespace_name
def initialize(name, service_account_name, namespace_name)
@name = name
@service_account_name = service_account_name
@namespace_name = namespace_name
end
def generate
::Kubeclient::Resource.new(metadata: metadata, type: service_acount_token_type)
end
private
# as per https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/#to-create-additional-api-tokens
def service_acount_token_type
'kubernetes.io/service-account-token'
end
def metadata
{
name: name,
namespace: namespace_name,
annotations: {
"kubernetes.io/service-account.name": service_account_name
}
}
end
end
end
end
......@@ -50,7 +50,7 @@ module GoogleApi
service.get_zone_cluster(project_id, zone, cluster_id, options: user_agent_header)
end
def projects_zones_clusters_create(project_id, zone, cluster_name, cluster_size, machine_type:)
def projects_zones_clusters_create(project_id, zone, cluster_name, cluster_size, machine_type:, legacy_abac:)
service = Google::Apis::ContainerV1::ContainerService.new
service.authorization = access_token
......@@ -63,7 +63,7 @@ module GoogleApi
"machine_type": machine_type
},
"legacy_abac": {
"enabled": true
"enabled": legacy_abac
}
}
}
......
......@@ -170,12 +170,14 @@ describe Projects::ClustersController do
end
describe 'POST create for new cluster' do
let(:legacy_abac_param) { 'true' }
let(:params) do
{
cluster: {
name: 'new-cluster',
provider_gcp_attributes: {
gcp_project_id: 'gcp-project-12345'
gcp_project_id: 'gcp-project-12345',
legacy_abac: legacy_abac_param
}
}
}
......@@ -201,6 +203,18 @@ describe Projects::ClustersController do
expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
expect(project.clusters.first).to be_gcp
expect(project.clusters.first).to be_kubernetes
expect(project.clusters.first.provider_gcp).to be_legacy_abac
end
context 'when legacy_abac param is false' do
let(:legacy_abac_param) { 'false' }
it 'creates a new cluster with legacy_abac_disabled' do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Providers::Gcp.count }
expect(project.clusters.first.provider_gcp).not_to be_legacy_abac
end
end
end
......
......@@ -33,6 +33,32 @@ describe 'Gcp Cluster', :js do
context 'when user filled form with valid parameters' do
subject { click_button 'Create Kubernetes cluster' }
shared_examples 'valid cluster gcp form' do
it 'users sees a form with the GCP token' do
expect(page).to have_selector(:css, 'form[data-token="token"]')
end
it 'user sees a cluster details page and creation status' do
subject
expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
Clusters::Cluster.last.provider.make_created!
expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine')
end
it 'user sees a error if something wrong during creation' do
subject
expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
Clusters::Cluster.last.provider.make_errored!('Something wrong!')
expect(page).to have_content('Something wrong!')
end
end
before do
allow_any_instance_of(GoogleApi::CloudPlatform::Client)
.to receive(:projects_zones_clusters_create) do
......@@ -56,28 +82,16 @@ describe 'Gcp Cluster', :js do
fill_in 'cluster[provider_gcp_attributes][machine_type]', with: 'n1-standard-2'
end
it 'users sees a form with the GCP token' do
expect(page).to have_selector(:css, 'form[data-token="token"]')
end
it 'user sees a cluster details page and creation status' do
subject
expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
Clusters::Cluster.last.provider.make_created!
expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine')
end
it 'user sees a error if something wrong during creation' do
subject
it_behaves_like 'valid cluster gcp form'
expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
context 'rbac_clusters feature flag is enabled' do
before do
stub_feature_flags(rbac_clusters: true)
Clusters::Cluster.last.provider.make_errored!('Something wrong!')
check 'cluster_provider_gcp_attributes_legacy_abac'
end
expect(page).to have_content('Something wrong!')
it_behaves_like 'valid cluster gcp form'
end
end
......
......@@ -21,42 +21,43 @@ describe 'User Cluster', :js do
end
context 'when user filled form with valid parameters' do
shared_examples 'valid cluster user form' do
it 'user sees a cluster details page' do
subject
expect(page).to have_content('Kubernetes cluster integration')
expect(page.find_field('cluster[name]').value).to eq('dev-cluster')
expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
.to have_content('http://example.com')
expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value)
.to have_content('my-token')
end
end
before do
fill_in 'cluster_name', with: 'dev-cluster'
fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com'
fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token'
click_button 'Add Kubernetes cluster'
end
it 'user sees a cluster details page' do
expect(page).to have_content('Kubernetes cluster integration')
expect(page.find_field('cluster[name]').value).to eq('dev-cluster')
expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
.to have_content('http://example.com')
expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value)
.to have_content('my-token')
end
end
subject { click_button 'Add Kubernetes cluster' }
context 'rbac_clusters feature flag is enabled' do
before do
stub_feature_flags(rbac_clusters: true)
it_behaves_like 'valid cluster user form'
fill_in 'cluster_name', with: 'dev-cluster'
fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com'
fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token'
check 'cluster_platform_kubernetes_attributes_authorization_type'
click_button 'Add Kubernetes cluster'
end
context 'rbac_clusters feature flag is enabled' do
before do
stub_feature_flags(rbac_clusters: true)
check 'cluster_platform_kubernetes_attributes_authorization_type'
end
it_behaves_like 'valid cluster user form'
it 'user sees a cluster details page' do
expect(page).to have_content('Kubernetes cluster integration')
expect(page.find_field('cluster[name]').value).to eq('dev-cluster')
expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
.to have_content('http://example.com')
expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value)
.to have_content('my-token')
expect(page.find_field('cluster[platform_kubernetes_attributes][authorization_type]', disabled: true)).to be_checked
it 'user sees a cluster details page with RBAC enabled' do
subject
expect(page.find_field('cluster[platform_kubernetes_attributes][authorization_type]', disabled: true)).to be_checked
end
end
end
......
......@@ -116,12 +116,14 @@ describe Gitlab::Kubernetes::KubeClient do
:get_config_map,
:get_pod,
:get_namespace,
:get_secret,
:get_service,
:get_service_account,
:delete_pod,
:create_config_map,
:create_namespace,
:create_pod,
:create_secret,
:create_service_account,
:update_config_map,
:update_service_account
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Kubernetes::ServiceAccountToken do
let(:name) { 'token-name' }
let(:service_account_name) { 'a_service_account' }
let(:namespace_name) { 'a_namespace' }
let(:service_account_token) { described_class.new(name, service_account_name, namespace_name) }
it { expect(service_account_token.name).to eq(name) }
it { expect(service_account_token.service_account_name).to eq(service_account_name) }
it { expect(service_account_token.namespace_name).to eq(namespace_name) }
describe '#generate' do
let(:resource) do
::Kubeclient::Resource.new(
metadata: {
name: name,
namespace: namespace_name,
annotations: {
'kubernetes.io/service-account.name': service_account_name
}
},
type: 'kubernetes.io/service-account-token'
)
end
subject { service_account_token.generate }
it 'should build a Kubeclient Resource' do
is_expected.to eq(resource)
end
end
end
......@@ -66,25 +66,30 @@ describe GoogleApi::CloudPlatform::Client do
describe '#projects_zones_clusters_create' do
subject do
client.projects_zones_clusters_create(
spy, spy, cluster_name, cluster_size, machine_type: machine_type)
project_id, zone, cluster_name, cluster_size, machine_type: machine_type, legacy_abac: legacy_abac)
end
let(:project_id) { 'project-123' }
let(:zone) { 'us-central1-a' }
let(:cluster_name) { 'test-cluster' }
let(:cluster_size) { 1 }
let(:machine_type) { 'n1-standard-2' }
let(:legacy_abac) { true }
let(:create_cluster_request_body) { double('Google::Apis::ContainerV1::CreateClusterRequest') }
let(:operation) { double }
before do
allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
.to receive(:create_cluster).with(any_args, options: user_agent_options)
.to receive(:create_cluster).with(any_args)
.and_return(operation)
end
it { is_expected.to eq(operation) }
it 'sets corresponded parameters' do
expect_any_instance_of(Google::Apis::ContainerV1::CreateClusterRequest)
.to receive(:initialize).with(
expect_any_instance_of(Google::Apis::ContainerV1::ContainerService)
.to receive(:create_cluster).with(project_id, zone, create_cluster_request_body, options: user_agent_options)
expect(Google::Apis::ContainerV1::CreateClusterRequest)
.to receive(:new).with(
{
"cluster": {
"name": cluster_name,
......@@ -96,9 +101,35 @@ describe GoogleApi::CloudPlatform::Client do
"enabled": true
}
}
} )
} ).and_return(create_cluster_request_body)
expect(subject).to eq operation
end
context 'create without legacy_abac' do
let(:legacy_abac) { false }
it 'sets corresponded parameters' do
expect_any_instance_of(Google::Apis::ContainerV1::ContainerService)
.to receive(:create_cluster).with(project_id, zone, create_cluster_request_body, options: user_agent_options)
expect(Google::Apis::ContainerV1::CreateClusterRequest)
.to receive(:new).with(
{
"cluster": {
"name": cluster_name,
"initial_node_count": cluster_size,
"node_config": {
"machine_type": machine_type
},
"legacy_abac": {
"enabled": false
}
}
} ).and_return(create_cluster_request_body)
subject
expect(subject).to eq operation
end
end
end
......
......@@ -74,6 +74,24 @@ describe Clusters::Providers::Gcp do
end
end
describe '#legacy_abac?' do
let(:gcp) { build(:cluster_provider_gcp) }
subject { gcp }
it 'should default to true' do
is_expected.to be_legacy_abac
end
context 'legacy_abac is set to false' do
let(:gcp) { build(:cluster_provider_gcp, legacy_abac: false) }
it 'is false' do
is_expected.not_to be_legacy_abac
end
end
end
describe '#state_machine' do
context 'when any => [:created]' do
let(:gcp) { build(:cluster_provider_gcp, :creating) }
......
require 'spec_helper'
describe Ci::FetchKubernetesTokenService do
describe '#execute' do
subject { described_class.new(api_url, ca_pem, username, password).execute }
let(:api_url) { 'http://111.111.111.111' }
let(:ca_pem) { '' }
let(:username) { 'admin' }
let(:password) { 'xxx' }
context 'when params correct' do
let(:token) { 'xxx.token.xxx' }
let(:secrets_json) do
[
{
'metadata': {
name: metadata_name
},
'data': {
'token': Base64.encode64(token)
}
}
]
end
before do
allow_any_instance_of(Kubeclient::Client)
.to receive(:get_secrets).and_return(secrets_json)
end
context 'when default-token exists' do
let(:metadata_name) { 'default-token-123' }
it { is_expected.to eq(token) }
end
context 'when default-token does not exist' do
let(:metadata_name) { 'another-token-123' }
it { is_expected.to be_nil }
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
......@@ -12,9 +12,11 @@ describe Clusters::Gcp::FinalizeCreationService do
let(:zone) { provider.zone }
let(:cluster_name) { cluster.name }
subject { described_class.new.execute(provider) }
shared_examples 'success' do
it 'configures provider and kubernetes' do
described_class.new.execute(provider)
subject
expect(provider).to be_created
end
......@@ -22,7 +24,7 @@ describe Clusters::Gcp::FinalizeCreationService do
shared_examples 'error' do
it 'sets an error to provider object' do
described_class.new.execute(provider)
subject
expect(provider.reload).to be_errored
end
......@@ -33,6 +35,7 @@ describe Clusters::Gcp::FinalizeCreationService do
let(:api_url) { 'https://' + endpoint }
let(:username) { 'sample-username' }
let(:password) { 'sample-password' }
let(:secret_name) { 'gitlab-token' }
before do
stub_cloud_platform_get_zone_cluster(
......@@ -43,60 +46,102 @@ describe Clusters::Gcp::FinalizeCreationService do
password: password
}
)
stub_kubeclient_discover(api_url)
end
context 'when suceeded to fetch kuberenetes token' do
let(:token) { 'sample-token' }
context 'service account and token created' do
before do
stub_kubeclient_get_secrets(
api_url,
{
token: Base64.encode64(token)
} )
stub_kubeclient_discover(api_url)
stub_kubeclient_create_service_account(api_url)
stub_kubeclient_create_secret(api_url)
end
it_behaves_like 'success'
shared_context 'kubernetes token successfully fetched' do
let(:token) { 'sample-token' }
before do
stub_kubeclient_get_secret(
api_url,
{
metadata_name: secret_name,
token: Base64.encode64(token)
} )
end
end
context 'provider legacy_abac is enabled' do
include_context 'kubernetes token successfully fetched'
it_behaves_like 'success'
it 'has corresponded data' do
described_class.new.execute(provider)
cluster.reload
provider.reload
platform.reload
it 'properly configures database models' do
subject
expect(provider.endpoint).to eq(endpoint)
expect(platform.api_url).to eq(api_url)
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.token).to eq(token)
cluster.reload
expect(provider.endpoint).to eq(endpoint)
expect(platform.api_url).to eq(api_url)
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).to be_abac
expect(platform.authorization_type).to eq('abac')
expect(platform.token).to eq(token)
end
end
end
context 'when default-token is not found' do
before do
stub_kubeclient_get_secrets(api_url, metadata_name: 'aaaa')
context 'provider legacy_abac is disabled' do
before do
provider.legacy_abac = false
end
include_context 'kubernetes token successfully fetched'
context 'cluster role binding created' do
before do
stub_kubeclient_create_cluster_role_binding(api_url)
end
it_behaves_like 'success'
it 'properly configures database models' do
subject
cluster.reload
expect(provider.endpoint).to eq(endpoint)
expect(platform.api_url).to eq(api_url)
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).to be_rbac
expect(platform.token).to eq(token)
end
end
end
it_behaves_like 'error'
end
context 'when token is empty' do
before do
stub_kubeclient_get_secret(api_url, token: '', metadata_name: secret_name)
end
context 'when token is empty' do
before do
stub_kubeclient_get_secrets(api_url, token: '')
it_behaves_like 'error'
end
it_behaves_like 'error'
end
context 'when failed to fetch kubernetes token' do
before do
stub_kubeclient_get_secret_error(api_url, secret_name)
end
context 'when failed to fetch kuberenetes token' do
before do
stub_kubeclient_get_secrets_error(api_url)
it_behaves_like 'error'
end
it_behaves_like 'error'
context 'when service account fails to create' do
before do
stub_kubeclient_create_service_account_error(api_url)
end
it_behaves_like 'error'
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do
include KubernetesHelpers
let(:service) { described_class.new(kubeclient, rbac: rbac) }
describe '#execute' do
let(:rbac) { false }
let(:api_url) { 'http://111.111.111.111' }
let(:username) { 'admin' }
let(:password) { 'xxx' }
let(:kubeclient) do
Gitlab::Kubernetes::KubeClient.new(
api_url,
['api', 'apis/rbac.authorization.k8s.io'],
auth_options: { username: username, password: password }
)
end
subject { service.execute }
context 'when params are correct' do
before do
stub_kubeclient_discover(api_url)
stub_kubeclient_create_service_account(api_url)
stub_kubeclient_create_secret(api_url)
end
shared_examples 'creates service account and token' do
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(
kind: 'ServiceAccount',
metadata: { name: 'gitlab', namespace: 'default' }
)
)
end
it 'creates a kubernetes secret of type ServiceAccountToken' do
subject
expect(WebMock).to have_requested(:post, api_url + '/api/v1/namespaces/default/secrets').with(
body: hash_including(
kind: 'Secret',
metadata: {
name: 'gitlab-token',
namespace: 'default',
annotations: {
'kubernetes.io/service-account.name': 'gitlab'
}
},
type: 'kubernetes.io/service-account-token'
)
)
end
end
context 'abac enabled cluster' do
it_behaves_like 'creates service account and token'
end
context 'rbac enabled cluster' do
let(:rbac) { true }
before do
stub_kubeclient_create_cluster_role_binding(api_url)
end
it_behaves_like 'creates service account and token'
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(
kind: 'ClusterRoleBinding',
metadata: { name: 'gitlab-admin' },
roleRef: {
apiGroup: 'rbac.authorization.k8s.io',
kind: 'ClusterRole',
name: 'cluster-admin'
},
subjects: [{ kind: 'ServiceAccount', namespace: 'default', name: 'gitlab' }]
)
)
end
end
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do
describe '#execute' do
let(:api_url) { 'http://111.111.111.111' }
let(:username) { 'admin' }
let(:password) { 'xxx' }
let(:kubeclient) do
Gitlab::Kubernetes::KubeClient.new(
api_url,
['api', 'apis/rbac.authorization.k8s.io'],
auth_options: { username: username, password: password }
)
end
subject { described_class.new(kubeclient).execute }
context 'when params correct' do
let(:decoded_token) { 'xxx.token.xxx' }
let(:token) { Base64.encode64(decoded_token) }
let(:secret_json) do
{
'metadata': {
name: 'gitlab-token'
},
'data': {
'token': token
}
}
end
before do
allow_any_instance_of(Kubeclient::Client)
.to receive(:get_secret).and_return(secret_json)
end
context 'when gitlab-token exists' do
let(:metadata_name) { 'gitlab-token' }
it { is_expected.to eq(decoded_token) }
end
context 'when gitlab-token does not exist' do
let(:secret_json) { {} }
it { is_expected.to be_nil }
end
context 'when token is nil' do
let(:token) { nil }
it { is_expected.to be_nil }
end
end
end
end
......@@ -33,31 +33,49 @@ module KubernetesHelpers
WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
end
def stub_kubeclient_get_secrets(api_url, **options)
WebMock.stub_request(:get, api_url + '/api/v1/secrets')
.to_return(kube_response(kube_v1_secrets_body(options)))
def stub_kubeclient_get_secret(api_url, namespace: 'default', **options)
options[:metadata_name] ||= "default-token-1"
WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}/secrets/#{options[:metadata_name]}")
.to_return(kube_response(kube_v1_secret_body(options)))
end
def stub_kubeclient_get_secrets_error(api_url)
WebMock.stub_request(:get, api_url + '/api/v1/secrets')
def stub_kubeclient_get_secret_error(api_url, name, namespace: 'default')
WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}/secrets/#{name}")
.to_return(status: [404, "Internal Server Error"])
end
def kube_v1_secrets_body(**options)
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_service_account_error(api_url, namespace: 'default')
WebMock.stub_request(:post, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts")
.to_return(status: [500, "Internal Server Error"])
end
def stub_kubeclient_create_secret(api_url, namespace: 'default')
WebMock.stub_request(:post, api_url + "/api/v1/namespaces/#{namespace}/secrets")
.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_secret_body(**options)
{
"kind" => "SecretList",
"apiVersion": "v1",
"items" => [
{
"metadata": {
"name": options[:metadata_name] || "default-token-1",
"namespace": "kube-system"
},
"data": {
"token": options[:token] || Base64.encode64('token-sample-123')
}
}
]
"metadata": {
"name": options[:metadata_name] || "default-token-1",
"namespace": "kube-system"
},
"data": {
"token": options[:token] || Base64.encode64('token-sample-123')
}
}
end
......@@ -68,6 +86,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 +99,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" }
]
}
......
......@@ -7,7 +7,8 @@ shared_context 'valid cluster create params' do
gcp_project_id: 'gcp-project',
zone: 'us-central1-a',
num_nodes: 1,
machine_type: 'machine_type-a'
machine_type: 'machine_type-a',
legacy_abac: 'true'
}
}
end
......@@ -29,6 +30,10 @@ shared_context 'invalid cluster create params' do
end
shared_examples 'create cluster service success' do
before do
stub_feature_flags(rbac_clusters: false)
end
it 'creates a cluster object and performs a worker' do
expect(ClusterProvisionWorker).to receive(:perform_async)
......@@ -44,6 +49,7 @@ shared_examples 'create cluster service success' do
expect(subject.provider.num_nodes).to eq(1)
expect(subject.provider.machine_type).to eq('machine_type-a')
expect(subject.provider.access_token).to eq(access_token)
expect(subject.provider).to be_legacy_abac
expect(subject.platform).to be_nil
end
end
......
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