From f26884a0a82c7540b0b81e59c05ea7d75fbd2dd8 Mon Sep 17 00:00:00 2001 From: James Fargher Date: Mon, 20 May 2019 08:52:14 +0100 Subject: [PATCH 1/3] Migrate Kubernetes service integration templates to clusters The migration uses active record model stubs so that field encryption can be more easily used. --- .../migrate_k8s_service_integration.yml | 5 + ...7153211_migrate_k8s_service_integration.rb | 102 ++++++++++++++ .../migrate_k8s_service_integration_spec.rb | 124 ++++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 changelogs/unreleased/migrate_k8s_service_integration.yml create mode 100644 db/post_migrate/20190517153211_migrate_k8s_service_integration.rb create mode 100644 spec/migrations/migrate_k8s_service_integration_spec.rb diff --git a/changelogs/unreleased/migrate_k8s_service_integration.yml b/changelogs/unreleased/migrate_k8s_service_integration.yml new file mode 100644 index 00000000000..57f03e6bdab --- /dev/null +++ b/changelogs/unreleased/migrate_k8s_service_integration.yml @@ -0,0 +1,5 @@ +--- +title: Migrate Kubernetes service integration templates to clusters +merge_request: 28534 +author: +type: added diff --git a/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb b/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb new file mode 100644 index 00000000000..f9f13d64be9 --- /dev/null +++ b/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +class MigrateK8sServiceIntegration < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + class Cluster < ActiveRecord::Base + self.table_name = 'clusters' + + has_one :platform_kubernetes, class_name: 'MigrateK8sServiceIntegration::PlatformsKubernetes' + + accepts_nested_attributes_for :platform_kubernetes + + enum cluster_type: { + instance_type: 1, + group_type: 2, + project_type: 3 + } + + enum platform_type: { + kubernetes: 1 + } + + enum provider_type: { + user: 0, + gcp: 1 + } + end + + class PlatformsKubernetes < ActiveRecord::Base + self.table_name = 'cluster_platforms_kubernetes' + + belongs_to :cluster, class_name: 'MigrateK8sServiceIntegration::Cluster' + + attr_encrypted :token, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-cbc' + end + + class Service < ActiveRecord::Base + include EachBatch + + self.table_name = 'services' + self.inheritance_column = :_type_disabled # Disable STI, otherwise KubernetesModel will be looked up + + belongs_to :project, class_name: 'MigrateK8sServiceIntegration::Project', foreign_key: :project_id + + scope :kubernetes_service_templates, -> do + where(category: 'deployment', type: 'KubernetesService', template: true) + end + + def api_url + parsed_properties['api_url'].presence + end + + def ca_pem + parsed_properties['ca_pem'] + end + + def namespace + parsed_properties['namespace'].presence + end + + def token + parsed_properties['token'].presence + end + + private + + def parsed_properties + @parsed_properties ||= JSON.parse(self.properties) + end + end + + def up + MigrateK8sServiceIntegration::Service.kubernetes_service_templates.find_each do |service| + next unless service.api_url && service.token + + MigrateK8sServiceIntegration::Cluster.create!( + enabled: service.active, + managed: false, + name: 'KubernetesService', + cluster_type: 'instance_type', + provider_type: 'user', + platform_type: 'kubernetes', + platform_kubernetes_attributes: { + api_url: service.api_url, + ca_cert: service.ca_pem, + namespace: service.namespace, + token: service.token + } + ) + end + end + + def down + # It is not possible to tell which instance-level clusters were created by + # this migration. The original data is intentionally left intact. + end +end diff --git a/spec/migrations/migrate_k8s_service_integration_spec.rb b/spec/migrations/migrate_k8s_service_integration_spec.rb new file mode 100644 index 00000000000..9195db55e86 --- /dev/null +++ b/spec/migrations/migrate_k8s_service_integration_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190517153211_migrate_k8s_service_integration.rb') + +describe MigrateK8sServiceIntegration, :migration do + context 'template service' do + context 'with namespace' do + let!(:service) do + MigrateK8sServiceIntegration::Service.create!( + active: true, + template: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"prod\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}" + ) + end + + let(:cluster) { MigrateK8sServiceIntegration::Cluster.instance_type.last! } + let(:platform) { cluster.platform_kubernetes } + + it 'migrates the KubernetesService template to Platform::Kubernetes' do + expect { migrate! }.to change { MigrateK8sServiceIntegration::Cluster.count }.by(1) + + expect(cluster).to be_enabled + expect(cluster).to be_user + expect(cluster).not_to be_managed + expect(cluster.environment_scope).to eq('*') + expect(platform.api_url).to eq('https://sample.kubernetes.com') + expect(platform.ca_cert).to eq('ca_pem-sample') + expect(platform.namespace).to eq('prod') + expect(platform.token).to eq('token-sample') + end + end + + context 'without namespace' do + let!(:service) do + MigrateK8sServiceIntegration::Service.create!( + active: true, + template: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}" + ) + end + + let(:cluster) { MigrateK8sServiceIntegration::Cluster.instance_type.last! } + let(:platform) { cluster.platform_kubernetes } + + it 'migrates the KubernetesService template to Platform::Kubernetes' do + expect { migrate! }.to change { MigrateK8sServiceIntegration::Cluster.count }.by(1) + + expect(cluster).to be_enabled + expect(cluster).to be_user + expect(cluster).not_to be_managed + expect(cluster.environment_scope).to eq('*') + expect(platform.api_url).to eq('https://sample.kubernetes.com') + expect(platform.ca_cert).to eq('ca_pem-sample') + expect(platform.namespace).to be_nil + expect(platform.token).to eq('token-sample') + end + end + + context 'with nullified parameters' do + let!(:service) do + MigrateK8sServiceIntegration::Service.create!( + active: true, + template: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{}" + ) + end + + it 'does not migrate the KubernetesService' do + expect { migrate! }.not_to change { MigrateK8sServiceIntegration::Cluster.count } + end + end + + context 'when disabled' do + let!(:service) do + MigrateK8sServiceIntegration::Service.create!( + active: false, + template: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"prod\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}" + ) + end + + let(:cluster) { MigrateK8sServiceIntegration::Cluster.instance_type.last! } + let(:platform) { cluster.platform_kubernetes } + + it 'migrates the KubernetesService template to Platform::Kubernetes' do + expect { migrate! }.to change { MigrateK8sServiceIntegration::Cluster.count }.by(1) + + expect(cluster).not_to be_enabled + expect(cluster).to be_user + expect(cluster).not_to be_managed + expect(cluster.environment_scope).to eq('*') + expect(platform.api_url).to eq('https://sample.kubernetes.com') + expect(platform.ca_cert).to eq('ca_pem-sample') + expect(platform.namespace).to eq('prod') + expect(platform.token).to eq('token-sample') + end + end + end + + context 'non-template service' do + let!(:service) do + MigrateK8sServiceIntegration::Service.create!( + active: true, + template: false, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"prod\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}" + ) + end + + it 'does not migrate the KubernetesService' do + expect { migrate! }.not_to change { MigrateK8sServiceIntegration::Cluster.count } + end + end +end -- GitLab From 1a25ad3e578c9f3a0682551b7bef0a554c2763b8 Mon Sep 17 00:00:00 2001 From: James Fargher Date: Fri, 24 May 2019 09:39:59 +0100 Subject: [PATCH 2/3] Allow a user defined namespace on non-gitlab managed clusters Since they are not GitLab managed we wont make assumptions about the namespaces used --- app/models/clusters/cluster.rb | 2 +- spec/models/clusters/cluster_spec.rb | 34 ++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 57a1e461b2d..e23fb9f85c1 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -208,7 +208,7 @@ module Clusters end def allow_user_defined_namespace? - project_type? + project_type? || !managed? end def kube_ingress_domain diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 4739e62289a..19ced0275a1 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -509,19 +509,43 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do subject { cluster.allow_user_defined_namespace? } context 'project type cluster' do - it { is_expected.to be_truthy } + context 'gitlab managed' do + it { is_expected.to be_truthy } + end + + context 'not managed' do + let(:cluster) { create(:cluster, :provided_by_gcp, managed: false) } + + it { is_expected.to be_truthy } + end end context 'group type cluster' do - let(:cluster) { create(:cluster, :provided_by_gcp, :group) } + context 'gitlab managed' do + let(:cluster) { create(:cluster, :provided_by_gcp, :group) } + + it { is_expected.to be_falsey } + end - it { is_expected.to be_falsey } + context 'not managed' do + let(:cluster) { create(:cluster, :provided_by_gcp, :group, managed: false) } + + it { is_expected.to be_truthy } + end end context 'instance type cluster' do - let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + context 'gitlab managed' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it { is_expected.to be_falsey } + end + + context 'not managed' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance, managed: false) } - it { is_expected.to be_falsey } + it { is_expected.to be_truthy } + end end end -- GitLab From c234e7312357388c1e088876772ecae22cf06297 Mon Sep 17 00:00:00 2001 From: James Fargher Date: Fri, 24 May 2019 09:59:32 +0100 Subject: [PATCH 3/3] When an instance cluster already exists migrate disabled Assume that if an instance level cluster already exists, then the KubernetesService was not being used, but allow the admin to re-enable it if required --- ...7153211_migrate_k8s_service_integration.rb | 4 +- .../migrate_k8s_service_integration_spec.rb | 37 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb b/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb index f9f13d64be9..4bd04edb239 100644 --- a/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb +++ b/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb @@ -75,11 +75,13 @@ class MigrateK8sServiceIntegration < ActiveRecord::Migration[5.1] end def up + has_instance_cluster = Cluster.instance_type.where(enabled: true).exists? + MigrateK8sServiceIntegration::Service.kubernetes_service_templates.find_each do |service| next unless service.api_url && service.token MigrateK8sServiceIntegration::Cluster.create!( - enabled: service.active, + enabled: !has_instance_cluster && service.active, managed: false, name: 'KubernetesService', cluster_type: 'instance_type', diff --git a/spec/migrations/migrate_k8s_service_integration_spec.rb b/spec/migrations/migrate_k8s_service_integration_spec.rb index 9195db55e86..4dd0c09632a 100644 --- a/spec/migrations/migrate_k8s_service_integration_spec.rb +++ b/spec/migrations/migrate_k8s_service_integration_spec.rb @@ -104,6 +104,43 @@ describe MigrateK8sServiceIntegration, :migration do expect(platform.token).to eq('token-sample') end end + + context 'when an instance cluster already exists' do + let!(:service) do + MigrateK8sServiceIntegration::Service.create!( + active: true, + template: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"prod\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}" + ) + end + + let!(:existing_cluster) do + MigrateK8sServiceIntegration::Cluster.create!( + name: 'test-cluster', + cluster_type: :instance_type, + managed: true, + provider_type: :user, + platform_type: :kubernetes + ) + end + let(:new_cluster) { MigrateK8sServiceIntegration::Cluster.instance_type.last! } + let(:platform) { new_cluster.platform_kubernetes } + + it 'migrates the KubernetesService template to disabled Platform::Kubernetes' do + expect { migrate! }.to change { MigrateK8sServiceIntegration::Cluster.count }.by(1) + + expect(new_cluster).not_to be_enabled + expect(new_cluster).to be_user + expect(new_cluster).not_to be_managed + expect(new_cluster.environment_scope).to eq('*') + expect(platform.api_url).to eq('https://sample.kubernetes.com') + expect(platform.ca_cert).to eq('ca_pem-sample') + expect(platform.namespace).to eq('prod') + expect(platform.token).to eq('token-sample') + end + end end context 'non-template service' do -- GitLab