diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 0f35b4f9c21..7570da3e0b1 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -29,7 +29,6 @@ class Projects::ClustersController < Projects::ApplicationController def new @cluster = Clusters::Cluster.new.tap do |cluster| cluster.build_provider_gcp - cluster.build_platform_kubernetes end end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index a3f6d20ba43..ca09b939f34 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -18,8 +18,6 @@ module Clusters accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true - validates :provider_type, presence: true - validates :platform_type, presence: true validates :name, cluster_name: true validate :restrict_modification, on: :update diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 4c3e270892e..3ad2ffe531d 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -28,7 +28,7 @@ module Clusters } # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned) - with_options presence: true, if: :active? do + with_options presence: true, if: :enabled? do validates :api_url, url: true, presence: true validates :token, presence: true end @@ -42,10 +42,6 @@ module Clusters delegate :project, to: :cluster, allow_nil: true delegate :enabled?, to: :cluster, allow_nil: true - def active? - enabled? && api_url.present? - end - class << self def namespace_for_project(project) "#{project.path}-#{project.id}" @@ -87,7 +83,7 @@ module Clusters return raise 'Kubernetes service already configured' unless manages_kubernetes_service? ensure_kubernetes_service.update!( - active: active?, + active: enabled?, api_url: api_url, namespace: namespace, token: token, diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb new file mode 100644 index 00000000000..44da87cb00c --- /dev/null +++ b/app/services/ci/fetch_kubernetes_token_service.rb @@ -0,0 +1,72 @@ +## +# 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 KubeException => 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 diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index d379870924a..53b13518771 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -9,10 +9,7 @@ module Clusters configure_provider configure_kubernetes - ActiveRecord::Base.transaction do - kubernetes.save! - provider.make_created! - end + provider.make_created! rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") rescue KubeException => e @@ -28,23 +25,21 @@ module Clusters end def configure_kubernetes - kubernetes.api_url = 'https://' + gke_cluster.endpoint - kubernetes.ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate) - kubernetes.username = gke_cluster.master_auth.username - kubernetes.password = gke_cluster.master_auth.password - kubernetes.token = request_kuberenetes_token + cluster.platform_type = :kubernetes + cluster.build_platform_kubernetes( + api_url: 'https://' + gke_cluster.endpoint, + ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), + username: gke_cluster.master_auth.username, + password: gke_cluster.master_auth.password, + token: request_kuberenetes_token) end def request_kuberenetes_token - kubernetes.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 + Ci::FetchKubernetesTokenService.new( + 'https://' + gke_cluster.endpoint, + Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), + gke_cluster.master_auth.username, + gke_cluster.master_auth.password) end def gke_cluster @@ -57,10 +52,6 @@ module Clusters def cluster @cluster ||= provider.cluster end - - def kubernetes - @kubernetes ||= cluster.platform_kubernetes - end end end end diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml index 6b9f63b7515..d8e5b55bb88 100644 --- a/app/views/projects/clusters/_form.html.haml +++ b/app/views/projects/clusters/_form.html.haml @@ -5,8 +5,7 @@ = s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page} = form_for @cluster, url: namespace_project_clusters_path(@project.namespace, @project, @cluster), as: :cluster do |field| - = field.hidden_field :platform_type, :value => 'kubernetes' - = field.hidden_field :provider_type, :value => 'gcp' + = field.hidden_field :provider_type, value: :gcp = form_errors(@cluster) .form-group = field.label :name, s_('ClusterIntegration|Cluster name') @@ -32,10 +31,5 @@ = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer') = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4' - = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| - .form-group - = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') - = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: Clusters::Platforms::Kubernetes.namespace_for_project(@project) - .form-group = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save' diff --git a/db/migrate/20171013094327_create_new_clusters_architectures.rb b/db/migrate/20171013094327_create_new_clusters_architectures.rb index a212288f859..b196aa1949c 100644 --- a/db/migrate/20171013094327_create_new_clusters_architectures.rb +++ b/db/migrate/20171013094327_create_new_clusters_architectures.rb @@ -5,8 +5,8 @@ class CreateNewClustersArchitectures < ActiveRecord::Migration create_table :clusters do |t| t.references :user, null: false, index: true, foreign_key: { on_delete: :nullify } - t.integer :provider_type, null: false - t.integer :platform_type, null: false + t.integer :provider_type + t.integer :platform_type t.datetime_with_timezone :created_at, null: false t.datetime_with_timezone :updated_at, null: false diff --git a/db/schema.rb b/db/schema.rb index 24f2d4b439c..d76977d45f2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -508,8 +508,8 @@ ActiveRecord::Schema.define(version: 20171017145932) do create_table "clusters", force: :cascade do |t| t.integer "user_id", null: false - t.integer "provider_type", null: false - t.integer "platform_type", null: false + t.integer "provider_type" + t.integer "platform_type" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "enabled", default: true diff --git a/spec/services/ci/fetch_kubernetes_token_service_spec.rb b/spec/services/ci/fetch_kubernetes_token_service_spec.rb new file mode 100644 index 00000000000..1d05c9671a9 --- /dev/null +++ b/spec/services/ci/fetch_kubernetes_token_service_spec.rb @@ -0,0 +1,64 @@ +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