diff options
author | Shinya Maeda <shinya@gitlab.com> | 2017-10-29 21:48:45 +0300 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2017-10-29 21:48:45 +0300 |
commit | 478e59fe8d82b99800a2613aa4d153bf692fbd6b (patch) | |
tree | 5f734aee006c7cfee86c8151e3b6f94846b15299 | |
parent | d0cff7f5855f91b5479f9fdaa39d8d95ec691a9e (diff) |
specs for models. Improved details.
-rw-r--r-- | app/models/clusters/cluster.rb | 26 | ||||
-rw-r--r-- | app/models/clusters/platforms/kubernetes.rb | 50 | ||||
-rw-r--r-- | app/models/clusters/project.rb | 4 | ||||
-rw-r--r-- | app/models/clusters/providers/gcp.rb | 3 | ||||
-rw-r--r-- | app/validators/cluster_name_validator.rb | 2 | ||||
-rw-r--r-- | app/views/projects/clusters/_form.html.haml | 2 | ||||
-rw-r--r-- | spec/factories/clusters/cluster.rb | 39 | ||||
-rw-r--r-- | spec/factories/clusters/platforms/gcp.rb | 28 | ||||
-rw-r--r-- | spec/factories/clusters/providers/kubernetes.rb | 18 | ||||
-rw-r--r-- | spec/factories/gcp/cluster.rb | 38 | ||||
-rw-r--r-- | spec/fixtures/clusters/sample_cert.pem | 33 | ||||
-rw-r--r-- | spec/models/clusters/cluster_spec.rb | 180 | ||||
-rw-r--r-- | spec/models/clusters/platforms/kubernetes_spec.rb | 304 | ||||
-rw-r--r-- | spec/models/clusters/project_spec.rb | 6 | ||||
-rw-r--r-- | spec/models/clusters/providers/gcp_spec.rb | 183 | ||||
-rw-r--r-- | spec/models/gcp/cluster_spec.rb | 264 |
16 files changed, 838 insertions, 342 deletions
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index f1eedad8795..4260fadb46d 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -6,15 +6,6 @@ module Clusters belongs_to :user - enum platform_type: { - kubernetes: 1 - } - - enum provider_type: { - user: 0, - gcp: 1 - } - has_many :cluster_projects, class_name: 'Clusters::Project' has_many :projects, through: :cluster_projects, class_name: '::Project' @@ -32,6 +23,18 @@ module Clusters delegate :status_name, to: :provider, allow_nil: true delegate :on_creation?, to: :provider, allow_nil: true + enum platform_type: { + kubernetes: 1 + } + + enum provider_type: { + user: 0, + gcp: 1 + } + + scope :enabled, -> { where(enabled: true) } + scope :disabled, -> { where(enabled: false) } + def provider return provider_gcp if gcp? end @@ -40,15 +43,12 @@ module Clusters return platform_kubernetes if kubernetes? end - def project - first_project - end - def first_project return @first_project if defined?(@first_project) @first_project = projects.first end + alias_method :project, :first_project private diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index d9f8927f7cc..b20b00ff51b 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -1,13 +1,11 @@ module Clusters module Platforms class Kubernetes < ActiveRecord::Base + include Gitlab::CurrentSettings include Gitlab::Kubernetes include ReactiveCaching self.table_name = 'cluster_platforms_kubernetes' - - TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze - self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.cluster_id] } belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster' @@ -22,6 +20,8 @@ module Clusters key: Gitlab::Application.secrets.db_key_base, algorithm: 'aes-256-cbc' + before_validation :enforce_namespace_to_lower_case + validates :namespace, allow_blank: true, length: 1..63, @@ -34,8 +34,19 @@ module Clusters validates :token, presence: true, on: :update after_save :clear_reactive_cache! - - before_validation :enforce_namespace_to_lower_case + + alias_attribute :ca_pem, :ca_cert + + delegate :project, to: :cluster, allow_nil: true + delegate :enabled?, to: :cluster, allow_nil: true + + alias_method :active?, :enabled? + + class << self + def namespace_for_project(project) + "#{project.path}-#{project.id}" + end + end def actual_namespace if namespace.present? @@ -45,6 +56,10 @@ module Clusters end end + def default_namespace + self.class.namespace_for_project(project) if project + end + def predefined_variables config = YAML.dump(kubeconfig) @@ -55,9 +70,9 @@ module Clusters { key: 'KUBECONFIG', value: config, public: false, file: true } ] - if ca_cert.present? - variables << { key: 'KUBE_CA_PEM', value: ca_cert, public: true } - variables << { key: 'KUBE_CA_PEM_FILE', value: ca_cert, public: true, file: true } + if ca_pem.present? + variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } + variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } end variables @@ -78,7 +93,7 @@ module Clusters # Caches resources in the namespace so other calls don't need to block on # network access def calculate_reactive_cache - return unless active? && cluster.project && !cluster.project.pending_delete? + return unless active? && project && !project.pending_delete? # We may want to cache extra things in the future { pods: read_pods } @@ -89,16 +104,7 @@ module Clusters url: api_url, namespace: actual_namespace, token: token, - ca_pem: ca_cert) - end - - def namespace_placeholder - default_namespace || TEMPLATE_PLACEHOLDER - end - - def default_namespace(project = nil) - project ||= cluster&.project - "#{project.path}-#{project.id}" if project + ca_pem: ca_pem) end def read_secrets @@ -123,9 +129,9 @@ module Clusters def kubeclient_ssl_options opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - if ca_cert.present? + if ca_pem.present? opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_cert)) + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) end opts @@ -166,7 +172,7 @@ module Clusters def terminal_auth { token: token, - ca_pem: ca_cert, + ca_pem: ca_pem, max_session_time: current_application_settings.terminal_max_session_time } end diff --git a/app/models/clusters/project.rb b/app/models/clusters/project.rb index 69088100420..eeb734b20b8 100644 --- a/app/models/clusters/project.rb +++ b/app/models/clusters/project.rb @@ -2,7 +2,7 @@ module Clusters class Project < ActiveRecord::Base self.table_name = 'cluster_projects' - belongs_to :cluster, inverse_of: :projects, class_name: 'Clusters::Cluster' - belongs_to :project, inverse_of: :project, class_name: 'Project' + belongs_to :cluster, class_name: 'Clusters::Cluster' + belongs_to :project, class_name: '::Project' end end diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index e4f109d2794..7700ba86f1a 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -55,7 +55,8 @@ module Clusters before_transition any => [:creating] do |provider, transition| operation_id = transition.args.first - provider.operation_id = operation_id if operation_id + raise 'operation_id is required' unless operation_id + provider.operation_id = operation_id end before_transition any => [:errored] do |provider, transition| diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb index 6c9850af30f..13ec342f399 100644 --- a/app/validators/cluster_name_validator.rb +++ b/app/validators/cluster_name_validator.rb @@ -8,7 +8,7 @@ class ClusterNameValidator < ActiveModel::EachValidator record.errors.add(attribute, " has to be present") end elsif record.gcp? - if record.persisted? && record.name != value + if record.persisted? && record.name_changed? record.errors.add(attribute, " can not be changed because it's synchronized with provider") end diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml index b3020513abf..6b9f63b7515 100644 --- a/app/views/projects/clusters/_form.html.haml +++ b/app/views/projects/clusters/_form.html.haml @@ -35,7 +35,7 @@ = 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: @cluster.platform_kubernetes.default_namespace(@project) + = 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/spec/factories/clusters/cluster.rb b/spec/factories/clusters/cluster.rb new file mode 100644 index 00000000000..8ba1eda8cc9 --- /dev/null +++ b/spec/factories/clusters/cluster.rb @@ -0,0 +1,39 @@ +FactoryGirl.define do + factory :cluster, class: Clusters::Cluster do + user + name 'test-cluster' + provider_type :user + platform_type :kubernetes + + trait :project do + after(:create) do |cluster, evaluator| + cluster.projects << create(:project) + end + end + + trait :provided_by_user do + provider_type :user + platform_type :kubernetes + platform_kubernetes + end + + trait :provided_by_gcp do + provider_type :gcp + platform_type :kubernetes + platform_kubernetes + + provider_gcp do + create(:provider_gcp, :created) + end + end + + trait :providing_by_gcp do + provider_type :gcp + platform_type :kubernetes + + provider_gcp do + create(:provider_gcp, :creating) + end + end + end +end diff --git a/spec/factories/clusters/platforms/gcp.rb b/spec/factories/clusters/platforms/gcp.rb new file mode 100644 index 00000000000..c135bbb20a4 --- /dev/null +++ b/spec/factories/clusters/platforms/gcp.rb @@ -0,0 +1,28 @@ +FactoryGirl.define do + factory :provider_gcp, class: Clusters::Providers::Gcp do + cluster + gcp_project_id 'test-gcp-project' + + trait :creating do + access_token 'access_token_123' + + after(:build) do |gcp, evaluator| + gcp.make_creating('operation-123') + end + end + + trait :created do + endpoint '111.111.111.111' + + after(:build) do |gcp, evaluator| + gcp.make_created + end + end + + trait :errored do + after(:build) do |gcp, evaluator| + gcp.make_errored('Something wrong') + end + end + end +end diff --git a/spec/factories/clusters/providers/kubernetes.rb b/spec/factories/clusters/providers/kubernetes.rb new file mode 100644 index 00000000000..b4d413d32c1 --- /dev/null +++ b/spec/factories/clusters/providers/kubernetes.rb @@ -0,0 +1,18 @@ +FactoryGirl.define do + factory :platform_kubernetes, class: Clusters::Platforms::Kubernetes do + cluster + api_url 'https://kubernetes.example.com' + ca_cert nil + token 'a' * 40 + username 'xxxxxx' + password 'xxxxxx' + namespace nil + + trait :ca_cert do + after(:create) do |platform_kubernetes, evaluator| + pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) + platform_kubernetes.ca_cert = File.read(pem_file) + end + end + end +end diff --git a/spec/factories/gcp/cluster.rb b/spec/factories/gcp/cluster.rb deleted file mode 100644 index 5c062737ffc..00000000000 --- a/spec/factories/gcp/cluster.rb +++ /dev/null @@ -1,38 +0,0 @@ -# FactoryGirl.define do -# factory :gcp_cluster, class: Gcp::Cluster do -# project -# user -# enabled true -# gcp_project_id 'gcp-project-12345' -# gcp_cluster_name 'test-cluster' -# gcp_cluster_zone 'us-central1-a' -# gcp_cluster_size 1 -# gcp_machine_type 'n1-standard-4' - -# trait :with_kubernetes_service do -# after(:create) do |cluster, evaluator| -# create(:kubernetes_service, project: cluster.project).tap do |service| -# cluster.update(service: service) -# end -# end -# end - -# trait :custom_project_namespace do -# project_namespace 'sample-app' -# end - -# trait :created_on_gke do -# status_event :make_created -# endpoint '111.111.111.111' -# ca_cert 'xxxxxx' -# kubernetes_token 'xxxxxx' -# username 'xxxxxx' -# password 'xxxxxx' -# end - -# trait :errored do -# status_event :make_errored -# status_reason 'general error' -# end -# end -# end diff --git a/spec/fixtures/clusters/sample_cert.pem b/spec/fixtures/clusters/sample_cert.pem new file mode 100644 index 00000000000..e39a2b34416 --- /dev/null +++ b/spec/fixtures/clusters/sample_cert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAOutg3Kf2y5dMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTcxMDI5MTgxOTU3WhcNMTgxMDI5MTgxOTU3WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAvQysroM3TLxaavadSPnFIltrYnxCnU4PvCR8971HMWXsq7Z4ShU4BbbE +8yp7oUFjulSwW6DhdIvnQb8ihLKictLmrA0isQqrD/iNpKZ6/lI4DGWw4QzrvMnW +V4yy2QZNpg9tzQHd4+xkeeIoG23RijDU/sPd5dqxF+rPHBfCVInmYvSzLvMhneNj +Bt6gV02gU9e9hsnMatsDvEbvWKp7wcbPot0nWrfZulx2QAWyXy+zG9mJQUds6yc0 +4agAeT9JEb/xtRgR/kS0aUHSGnfSnhZiEn17s0PhTmbu7qSHgzgB+7oJrC9jPoUh +S2Wo3n0xykAjHrA8wC/Ddw3L38S41VQ58GEfNchistPswyMmXo/Oenv9P3s/kCOI +fndiksFNdqVo51y9Vjngj589hpOseFDyKmWPIEQZ9kxW/crjP6RZWWLHgz26KtxZ +uJaoYL8VBbYfrk/bucw0Ma2GEOp8rTsBE7SvgejXZa78q+381Kzc/utW6VwSXqzY +xeIitft0rXi17SZ+XoiTkIXtHn0ZwMtOXNDBADTpFmKa6wVACQilvcpOYD8gUHyH +pB+EDRdST3M4Fiq1MBAVhk8Lj3tHSJ/1ymeF1PWSu57AnJlzerzq2fcfPotNNd37 +ZPNkPh0kxPLwxbAyrHflzx9qVVdI1irY9055mNSnhzlec4qJ9cECAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUnVa5dYPoIG/3+qXml0bX8+N16GwwdQYDVR0jBG4wbIAUnVa5 +dYPoIG/3+qXml0bX8+N16GyhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDr +rYNyn9suXTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQAUg4cyxXi1 +VR8ejTpaAruRyJ1pEG9Kc3kiIRXODy60z3hJXnx9LkScPkWGiuL5XacfZ2rMd4bw +oVXIyi8U1UHWfAH8EZdrFKkU92jCiL5soHUONxLAvQEJ/FTR/qijrpzLCxXBdVQE +xFEDWUu6rxLFyjEwzwnRTLgpjR606fdb7qXHkuAMvZ/ezJj8j97hok3Odpn4lr2H +6hMTpK7HmDBX+kmdJJ+yBrm9hG1Pzpl7QU0dkxZ+qJNFjYMLnziiTwkv0c5ZaA9E +NykZUcOv3Sjb6spu1A/E2BSq4WTjkIjrogFlfimE1vmUmObTRJOqUB0Vky1kHEwN +pg7QqIJQmof1EAIaSM/YpUWXyumBwGLDUEud1JUz05In9Q4IZjEwZSJwbQW4fUia +A93m9rk3Lw3xsFcaUdPMFIXk0rPoF1IgmV/oqb0gK95lOWRLbN+AV8qpKPpcKXOc +TkIdFE47ZisEDhIdF6wC1izEMLeMEsPAO7/Y6MY4nRxsinSe95lRaw+yQpzx+mvJ +Q7n1kiHI9Pd5M3+CiQda0d/GO1o5ORJnUGJRvr9HKuNmE7Lif0As/N0AlywjzE7A +6Z8AEiWyRV1ffshu1k2UKmzvZuZeGGKRtrIjbJIRAtpRVtVZZGzhq5/sojCLoJ+u +texqFBUo/4mFRZa4pDItUdyOlDy2/LO/ag== +-----END CERTIFICATE----- diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb new file mode 100644 index 00000000000..e53ce8497f5 --- /dev/null +++ b/spec/models/clusters/cluster_spec.rb @@ -0,0 +1,180 @@ +require 'spec_helper' + +describe Clusters::Cluster do + it { is_expected.to belong_to(:user) } + it { is_expected.to have_many(:projects) } + it { is_expected.to have_one(:provider_gcp) } + it { is_expected.to have_one(:platform_kubernetes) } + it { is_expected.to delegate_method(:status).to(:provider) } + it { is_expected.to delegate_method(:status_reason).to(:provider) } + it { is_expected.to delegate_method(:status_name).to(:provider) } + it { is_expected.to delegate_method(:on_creation?).to(:provider) } + it { is_expected.to respond_to :project } + + describe '.enabled' do + subject { described_class.enabled } + + let!(:cluster) { create(:cluster, enabled: true) } + + before do + create(:cluster, enabled: false) + end + + it { is_expected.to contain_exactly(cluster) } + end + + describe '.disabled' do + subject { described_class.disabled } + + let!(:cluster) { create(:cluster, enabled: false) } + + before do + create(:cluster, enabled: true) + end + + it { is_expected.to contain_exactly(cluster) } + end + + describe 'validation' do + subject { cluster.valid? } + + context 'when validates name' do + context 'when provided by user' do + let!(:cluster) { build(:cluster, :provided_by_user, name: name) } + + context 'when name is empty' do + let(:name) { '' } + + it { is_expected.to be_falsey } + end + + context 'when name is nil' do + let(:name) { nil } + + it { is_expected.to be_falsey } + end + + context 'when name is present' do + let(:name) { 'cluster-name-1' } + + it { is_expected.to be_truthy } + end + end + + context 'when provided by gcp' do + let!(:cluster) { build(:cluster, :provided_by_gcp, name: name) } + + context 'when name is shorter than 1' do + let(:name) { '' } + + it { is_expected.to be_falsey } + end + + context 'when name is longer than 63' do + let(:name) { 'a' * 64 } + + it { is_expected.to be_falsey } + end + + context 'when name includes invalid character' do + let(:name) { '!!!!!!' } + + it { is_expected.to be_falsey } + end + + context 'when name is present' do + let(:name) { 'cluster-name-1' } + + it { is_expected.to be_truthy } + end + + context 'when record is persisted' do + let(:name) { 'cluster-name-1' } + + before do + cluster.save! + end + + context 'when name is changed' do + before do + cluster.name = 'new-cluster-name' + end + + it { is_expected.to be_falsey } + end + + context 'when name is same' do + before do + cluster.name = name + end + + it { is_expected.to be_truthy } + end + end + end + end + + context 'when validates restrict_modification' do + context 'when creation is on going' do + let!(:cluster) { create(:cluster, :providing_by_gcp) } + + it { expect(cluster.update(enabled: false)).to be_falsey } + end + + context 'when creation is done' do + let!(:cluster) { create(:cluster, :provided_by_gcp) } + + it { expect(cluster.update(enabled: false)).to be_truthy } + end + end + end + + describe '#provider' do + subject { cluster.provider } + + context 'when provider is gcp' do + let(:cluster) { create(:cluster, :provided_by_gcp) } + + it 'returns a provider' do + is_expected.to eq(cluster.provider_gcp) + expect(subject.class.name.deconstantize).to eq(Clusters::Providers.to_s) + end + end + + context 'when provider is user' do + let(:cluster) { create(:cluster, :provided_by_user) } + + it { is_expected.to be_nil } + end + end + + describe '#platform' do + subject { cluster.platform } + + context 'when platform is kubernetes' do + let(:cluster) { create(:cluster, :provided_by_user) } + + it 'returns a platform' do + is_expected.to eq(cluster.platform_kubernetes) + expect(subject.class.name.deconstantize).to eq(Clusters::Platforms.to_s) + end + end + end + + describe '#first_project' do + subject { cluster.first_project } + + context 'when cluster belongs to a project' do + let(:cluster) { create(:cluster, :project) } + let(:project) { Clusters::Project.find_by_cluster_id(cluster.id).project } + + it { is_expected.to eq(project) } + end + + context 'when cluster does not belong to projects' do + let(:cluster) { create(:cluster) } + + it { is_expected.to be_nil } + end + end +end diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb new file mode 100644 index 00000000000..ec6ecee6ff2 --- /dev/null +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -0,0 +1,304 @@ +require 'spec_helper' + +describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching do + include KubernetesHelpers + include ReactiveCachingHelpers + + it { is_expected.to belong_to(:cluster) } + it { is_expected.to be_kind_of(Gitlab::Kubernetes) } + it { is_expected.to be_kind_of(ReactiveCaching) } + it { is_expected.to respond_to :ca_pem } + + describe 'before_validation' do + context 'when namespace includes upper case' do + let(:kubernetes) { create(:platform_kubernetes, namespace: namespace) } + let(:namespace) { 'ABC' } + + it 'converts to lower case' do + expect(kubernetes.namespace).to eq('abc') + end + end + end + + describe 'validation' do + subject { kubernetes.valid? } + + context 'when validates namespace' do + let(:kubernetes) { build(:platform_kubernetes, namespace: namespace) } + + context 'when namespace is blank' do + let(:namespace) { '' } + + it { is_expected.to be_truthy } + end + + context 'when namespace is longer than 63' do + let(:namespace) { 'a' * 64 } + + it { is_expected.to be_falsey } + end + + context 'when namespace includes invalid character' do + let(:namespace) { '!!!!!!' } + + it { is_expected.to be_falsey } + end + + context 'when namespace is vaild' do + let(:namespace) { 'namespace-123' } + + it { is_expected.to be_truthy } + end + end + + context 'when validates api_url' do + context 'when updates a record' do + let(:kubernetes) { create(:platform_kubernetes) } + + before do + kubernetes.api_url = api_url + end + + context 'when api_url is invalid url' do + let(:api_url) { '!!!!!!' } + + it { expect(kubernetes.save).to be_falsey } + end + + context 'when api_url is nil' do + let(:api_url) { nil } + + it { expect(kubernetes.save).to be_falsey } + end + + context 'when api_url is valid url' do + let(:api_url) { 'https://111.111.111.111' } + + it { expect(kubernetes.save).to be_truthy } + end + end + + context 'when creates a record' do + let(:kubernetes) { build(:platform_kubernetes) } + + before do + kubernetes.api_url = api_url + end + + context 'when api_url is nil' do + let(:api_url) { nil } + + it { expect(kubernetes.save).to be_truthy } + end + end + end + + context 'when validates token' do + context 'when updates a record' do + let(:kubernetes) { create(:platform_kubernetes) } + + before do + kubernetes.token = token + end + + context 'when token is nil' do + let(:token) { nil } + + it { expect(kubernetes.save).to be_falsey } + end + end + + context 'when creates a record' do + let(:kubernetes) { build(:platform_kubernetes) } + + before do + kubernetes.token = token + end + + context 'when token is nil' do + let(:token) { nil } + + it { expect(kubernetes.save).to be_truthy } + end + end + end + end + + describe '#actual_namespace' do + subject { kubernetes.actual_namespace } + + let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) } + let(:kubernetes) { create(:platform_kubernetes, namespace: namespace) } + + context 'when namespace is present' do + let(:namespace) { 'namespace-123' } + + it { is_expected.to eq(namespace) } + end + + context 'when namespace is not present' do + let(:namespace) { nil } + + it { is_expected.to eq("#{cluster.project.path}-#{cluster.project.id}") } + end + end + + describe '.namespace_for_project' do + subject { described_class.namespace_for_project(project) } + + let(:project) { create(:project) } + + it { is_expected.to eq("#{project.path}-#{project.id}") } + end + + describe '#default_namespace' do + subject { kubernetes.default_namespace } + + let(:kubernetes) { create(:platform_kubernetes) } + + context 'when cluster belongs to a project' do + let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) } + + it { is_expected.to eq("#{cluster.project.path}-#{cluster.project.id}") } + end + + context 'when cluster belongs to nothing' do + let!(:cluster) { create(:cluster, platform_kubernetes: kubernetes) } + + it { is_expected.to be_nil } + end + end + + describe '#predefined_variables' do + let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) } + let(:kubernetes) { create(:platform_kubernetes, api_url: api_url, ca_cert: ca_pem, token: token) } + let(:api_url) { 'https://kube.domain.com' } + let(:ca_pem) { 'CA PEM DATA' } + let(:token) { 'token' } + + let(:kubeconfig) do + config_file = expand_fixture_path('config/kubeconfig.yml') + config = YAML.load(File.read(config_file)) + config.dig('users', 0, 'user')['token'] = token + config.dig('contexts', 0, 'context')['namespace'] = namespace + config.dig('clusters', 0, 'cluster')['certificate-authority-data'] = + Base64.strict_encode64(ca_pem) + + YAML.dump(config) + end + + shared_examples 'setting variables' do + it 'sets the variables' do + expect(kubernetes.predefined_variables).to include( + { key: 'KUBE_URL', value: api_url, public: true }, + { key: 'KUBE_TOKEN', value: token, public: false }, + { key: 'KUBE_NAMESPACE', value: namespace, public: true }, + { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true }, + { key: 'KUBE_CA_PEM', value: ca_pem, public: true }, + { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } + ) + end + end + + context 'namespace is provided' do + let(:namespace) { 'my-project' } + + before do + kubernetes.namespace = namespace + end + + it_behaves_like 'setting variables' + end + + context 'no namespace provided' do + let(:namespace) { kubernetes.actual_namespace } + + it_behaves_like 'setting variables' + + it 'sets the KUBE_NAMESPACE' do + kube_namespace = kubernetes.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' } + + expect(kube_namespace).not_to be_nil + expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/) + end + end + end + + describe '#terminals' do + subject { service.terminals(environment) } + + let!(:cluster) { create(:cluster, :project, platform_kubernetes: service) } + let(:project) { cluster.project } + let(:service) { create(:platform_kubernetes) } + let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") } + + context 'with invalid pods' do + it 'returns no terminals' do + stub_reactive_cache(service, pods: [{ "bad" => "pod" }]) + + is_expected.to be_empty + end + end + + context 'with valid pods' do + let(:pod) { kube_pod(app: environment.slug) } + let(:terminals) { kube_terminals(service, pod) } + + before do + stub_reactive_cache( + service, + pods: [pod, pod, kube_pod(app: "should-be-filtered-out")] + ) + end + + it 'returns terminals' do + is_expected.to eq(terminals + terminals) + end + + it 'uses max session time from settings' do + stub_application_setting(terminal_max_session_time: 600) + + times = subject.map { |terminal| terminal[:max_session_time] } + expect(times).to eq [600, 600, 600, 600] + end + end + end + + describe '#calculate_reactive_cache' do + subject { service.calculate_reactive_cache } + + let!(:cluster) { create(:cluster, :project, enabled: enabled, platform_kubernetes: service) } + let(:service) { create(:platform_kubernetes, :ca_cert) } + let(:enabled) { true } + + context 'when cluster is disabled' do + let(:enabled) { false } + + it { is_expected.to be_nil } + end + + context 'when kubernetes responds with valid pods' do + before do + stub_kubeclient_pods + end + + it { is_expected.to eq(pods: [kube_pod]) } + end + + context 'when kubernetes responds with 500s' do + before do + stub_kubeclient_pods(status: 500) + end + + it { expect { subject }.to raise_error(KubeException) } + end + + context 'when kubernetes responds with 404s' do + before do + stub_kubeclient_pods(status: 404) + end + + it { is_expected.to eq(pods: []) } + end + end +end diff --git a/spec/models/clusters/project_spec.rb b/spec/models/clusters/project_spec.rb new file mode 100644 index 00000000000..7d75d6ab345 --- /dev/null +++ b/spec/models/clusters/project_spec.rb @@ -0,0 +1,6 @@ +require 'spec_helper' + +describe Clusters::Project do + it { is_expected.to belong_to(:cluster) } + it { is_expected.to belong_to(:project) } +end diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb new file mode 100644 index 00000000000..99eb8c46e9a --- /dev/null +++ b/spec/models/clusters/providers/gcp_spec.rb @@ -0,0 +1,183 @@ +require 'spec_helper' + +describe Clusters::Providers::Gcp do + it { is_expected.to belong_to(:cluster) } + it { is_expected.to validate_presence_of(:zone) } + + describe 'default_value_for' do + let(:gcp) { build(:provider_gcp) } + + it "has default value" do + expect(gcp.zone).to eq('us-central1-a') + expect(gcp.num_nodes).to eq(3) + expect(gcp.machine_type).to eq('n1-standard-4') + end + end + + describe 'validation' do + subject { gcp.valid? } + + context 'when validates gcp_project_id' do + let(:gcp) { build(:provider_gcp, gcp_project_id: gcp_project_id) } + + context 'when gcp_project_id is shorter than 1' do + let(:gcp_project_id) { '' } + + it { is_expected.to be_falsey } + end + + context 'when gcp_project_id is longer than 63' do + let(:gcp_project_id) { 'a' * 64 } + + it { is_expected.to be_falsey } + end + + context 'when gcp_project_id includes invalid character' do + let(:gcp_project_id) { '!!!!!!' } + + it { is_expected.to be_falsey } + end + + context 'when gcp_project_id is valid' do + let(:gcp_project_id) { 'gcp-project-1' } + + it { is_expected.to be_truthy } + end + end + + context 'when validates num_nodes' do + let(:gcp) { build(:provider_gcp, num_nodes: num_nodes) } + + context 'when num_nodes is string' do + let(:num_nodes) { 'A3' } + + it { is_expected.to be_falsey } + end + + context 'when num_nodes is nil' do + let(:num_nodes) { nil } + + it { is_expected.to be_falsey } + end + + context 'when num_nodes is smaller than 1' do + let(:num_nodes) { 0 } + + it { is_expected.to be_falsey } + end + + context 'when num_nodes is valid' do + let(:num_nodes) { 3 } + + it { is_expected.to be_truthy } + end + end + end + + describe '#state_machine' do + context 'when any => [:created]' do + let(:gcp) { build(:provider_gcp, :creating) } + + before do + gcp.make_created + end + + it 'nullify access_token and operation_id' do + expect(gcp.access_token).to be_nil + expect(gcp.operation_id).to be_nil + expect(gcp).to be_created + end + end + + context 'when any => [:creating]' do + let(:gcp) { build(:provider_gcp) } + + context 'when operation_id is present' do + let(:operation_id) { 'operation-xxx' } + + before do + gcp.make_creating(operation_id) + end + + it 'sets operation_id' do + expect(gcp.operation_id).to eq(operation_id) + expect(gcp).to be_creating + end + end + + context 'when operation_id is nil' do + let(:operation_id) { nil } + + it 'raises an error' do + expect { gcp.make_creating(operation_id) } + .to raise_error('operation_id is required') + end + end + end + + context 'when any => [:errored]' do + let(:gcp) { build(:provider_gcp, :creating) } + let(:status_reason) { 'err msg' } + + it 'nullify access_token and operation_id' do + gcp.make_errored(status_reason) + + expect(gcp.access_token).to be_nil + expect(gcp.operation_id).to be_nil + expect(gcp.status_reason).to eq(status_reason) + expect(gcp).to be_errored + end + + context 'when status_reason is nil' do + let(:gcp) { build(:provider_gcp, :errored) } + + it 'does not set status_reason' do + gcp.make_errored(nil) + + expect(gcp.status_reason).not_to be_nil + end + end + end + end + + describe '#on_creation?' do + subject { gcp.on_creation? } + + context 'when status is creating' do + let(:gcp) { create(:provider_gcp, :creating) } + + it { is_expected.to be_truthy } + end + + context 'when status is created' do + let(:gcp) { create(:provider_gcp, :created) } + + it { is_expected.to be_falsey } + end + end + + describe '#api_client' do + subject { gcp.api_client } + + context 'when status is creating' do + let(:gcp) { build(:provider_gcp, :creating) } + + it 'returns Cloud Platform API clinet' do + expect(subject).to be_an_instance_of(GoogleApi::CloudPlatform::Client) + expect(subject.access_token).to eq(gcp.access_token) + end + end + + context 'when status is created' do + let(:gcp) { build(:provider_gcp, :created) } + + it { is_expected.to be_nil } + end + + context 'when status is errored' do + let(:gcp) { build(:provider_gcp, :errored) } + + it { is_expected.to be_nil } + end + end +end diff --git a/spec/models/gcp/cluster_spec.rb b/spec/models/gcp/cluster_spec.rb deleted file mode 100644 index 8f39fff6394..00000000000 --- a/spec/models/gcp/cluster_spec.rb +++ /dev/null @@ -1,264 +0,0 @@ -require 'spec_helper' - -describe Gcp::Cluster do - it { is_expected.to belong_to(:project) } - it { is_expected.to belong_to(:user) } - it { is_expected.to belong_to(:service) } - - it { is_expected.to validate_presence_of(:gcp_cluster_zone) } - - describe '.enabled' do - subject { described_class.enabled } - - let!(:cluster) { create(:gcp_cluster, enabled: true) } - - before do - create(:gcp_cluster, enabled: false) - end - - it { is_expected.to contain_exactly(cluster) } - end - - describe '.disabled' do - subject { described_class.disabled } - - let!(:cluster) { create(:gcp_cluster, enabled: false) } - - before do - create(:gcp_cluster, enabled: true) - end - - it { is_expected.to contain_exactly(cluster) } - end - - describe '#default_value_for' do - let(:cluster) { described_class.new } - - it { expect(cluster.gcp_cluster_zone).to eq('us-central1-a') } - it { expect(cluster.gcp_cluster_size).to eq(3) } - it { expect(cluster.gcp_machine_type).to eq('n1-standard-4') } - end - - describe '#validates' do - subject { cluster.valid? } - - context 'when validates gcp_project_id' do - let(:cluster) { build(:gcp_cluster, gcp_project_id: gcp_project_id) } - - context 'when valid' do - let(:gcp_project_id) { 'gcp-project-12345' } - - it { is_expected.to be_truthy } - end - - context 'when empty' do - let(:gcp_project_id) { '' } - - it { is_expected.to be_falsey } - end - - context 'when too long' do - let(:gcp_project_id) { 'A' * 64 } - - it { is_expected.to be_falsey } - end - - context 'when includes abnormal character' do - let(:gcp_project_id) { '!!!!!!' } - - it { is_expected.to be_falsey } - end - end - - context 'when validates gcp_cluster_name' do - let(:cluster) { build(:gcp_cluster, gcp_cluster_name: gcp_cluster_name) } - - context 'when valid' do - let(:gcp_cluster_name) { 'test-cluster' } - - it { is_expected.to be_truthy } - end - - context 'when empty' do - let(:gcp_cluster_name) { '' } - - it { is_expected.to be_falsey } - end - - context 'when too long' do - let(:gcp_cluster_name) { 'A' * 64 } - - it { is_expected.to be_falsey } - end - - context 'when includes abnormal character' do - let(:gcp_cluster_name) { '!!!!!!' } - - it { is_expected.to be_falsey } - end - end - - context 'when validates gcp_cluster_size' do - let(:cluster) { build(:gcp_cluster, gcp_cluster_size: gcp_cluster_size) } - - context 'when valid' do - let(:gcp_cluster_size) { 1 } - - it { is_expected.to be_truthy } - end - - context 'when zero' do - let(:gcp_cluster_size) { 0 } - - it { is_expected.to be_falsey } - end - end - - context 'when validates project_namespace' do - let(:cluster) { build(:gcp_cluster, project_namespace: project_namespace) } - - context 'when valid' do - let(:project_namespace) { 'default-namespace' } - - it { is_expected.to be_truthy } - end - - context 'when empty' do - let(:project_namespace) { '' } - - it { is_expected.to be_truthy } - end - - context 'when too long' do - let(:project_namespace) { 'A' * 64 } - - it { is_expected.to be_falsey } - end - - context 'when includes abnormal character' do - let(:project_namespace) { '!!!!!!' } - - it { is_expected.to be_falsey } - end - end - - context 'when validates restrict_modification' do - let(:cluster) { create(:gcp_cluster) } - - before do - cluster.make_creating! - end - - context 'when created' do - before do - cluster.make_created! - end - - it { is_expected.to be_truthy } - end - - context 'when creating' do - it { is_expected.to be_falsey } - end - end - end - - describe '#state_machine' do - let(:cluster) { build(:gcp_cluster) } - - context 'when transits to created state' do - before do - cluster.gcp_token = 'tmp' - cluster.gcp_operation_id = 'tmp' - cluster.make_created! - end - - it 'nullify gcp_token and gcp_operation_id' do - expect(cluster.gcp_token).to be_nil - expect(cluster.gcp_operation_id).to be_nil - expect(cluster).to be_created - end - end - - context 'when transits to errored state' do - let(:reason) { 'something wrong' } - - before do - cluster.make_errored!(reason) - end - - it 'sets status_reason' do - expect(cluster.status_reason).to eq(reason) - expect(cluster).to be_errored - end - end - end - - describe '#project_namespace_placeholder' do - subject { cluster.project_namespace_placeholder } - - let(:cluster) { create(:gcp_cluster) } - - it 'returns a placeholder' do - is_expected.to eq("#{cluster.project.path}-#{cluster.project.id}") - end - end - - describe '#on_creation?' do - subject { cluster.on_creation? } - - let(:cluster) { create(:gcp_cluster) } - - context 'when status is creating' do - before do - cluster.make_creating! - end - - it { is_expected.to be_truthy } - end - - context 'when status is created' do - before do - cluster.make_created! - end - - it { is_expected.to be_falsey } - end - end - - describe '#api_url' do - subject { cluster.api_url } - - let(:cluster) { create(:gcp_cluster, :created_on_gke) } - let(:api_url) { 'https://' + cluster.endpoint } - - it { is_expected.to eq(api_url) } - end - - describe '#restrict_modification' do - subject { cluster.restrict_modification } - - let(:cluster) { create(:gcp_cluster) } - - context 'when status is created' do - before do - cluster.make_created! - end - - it { is_expected.to be_truthy } - end - - context 'when status is creating' do - before do - cluster.make_creating! - end - - it { is_expected.to be_falsey } - - it 'sets error' do - is_expected.to be_falsey - expect(cluster.errors).not_to be_empty - end - end - end -end |