diff options
author | Shinya Maeda <shinya@gitlab.com> | 2017-11-21 21:09:55 +0900 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2017-11-28 20:47:59 +0900 |
commit | 8a55d2c5526ee5cb93bb40075dbbeaa6fcb01f91 (patch) | |
tree | b67163385d4e66e3a35fc4cf5da079dfca22ea55 | |
parent | 25a3a1838ab3224be0b6c658ea16fd1b37135372 (diff) | |
download | gitlab-ce-8a55d2c5526ee5cb93bb40075dbbeaa6fcb01f91.tar.gz |
Revert KubernetesService logic in Platforms::Kubernetes
-rw-r--r-- | app/models/clusters/platforms/kubernetes.rb | 123 | ||||
-rw-r--r-- | spec/models/clusters/platforms/kubernetes_spec.rb | 135 |
2 files changed, 258 insertions, 0 deletions
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 6dc1ee810d3..a1e686d0d12 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -1,7 +1,12 @@ module Clusters module Platforms class Kubernetes < ActiveRecord::Base + include Gitlab::CurrentSettings + include Gitlab::Kubernetes + include ReactiveCaching + self.table_name = 'cluster_platforms_kubernetes' + self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.cluster_id] } belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster' @@ -29,6 +34,8 @@ module Clusters validates :api_url, url: true, presence: true validates :token, presence: true + after_save :clear_reactive_cache! + # TODO: Glue code till we migrate Kubernetes Integration into Platforms::Kubernetes after_destroy :destroy_kubernetes_integration! @@ -55,6 +62,80 @@ module Clusters self.class.namespace_for_project(project) if project end + def predefined_variables + config = YAML.dump(kubeconfig) + + variables = [ + { key: 'KUBE_URL', value: api_url, public: true }, + { key: 'KUBE_TOKEN', value: token, public: false }, + { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true }, + { key: 'KUBECONFIG', value: config, public: false, 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 + end + + # Constructs a list of terminals from the reactive cache + # + # Returns nil if the cache is empty, in which case you should try again a + # short time later + def terminals(environment) + with_reactive_cache do |data| + pods = filter_by_label(data[:pods], app: environment.slug) + terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) } + terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) } + end + end + + # Caches resources in the namespace so other calls don't need to block on + # network access + def calculate_reactive_cache + return unless active? && project && !project.pending_delete? + + # We may want to cache extra things in the future + { pods: read_pods } + end + + def kubeconfig + to_kubeconfig( + url: api_url, + namespace: actual_namespace, + token: token, + ca_pem: ca_pem) + end + + def read_secrets + kubeclient = build_kubeclient! + + kubeclient.get_secrets.as_json + end + + # Returns a hash of all pods in the namespace + def read_pods + kubeclient = build_kubeclient! + + kubeclient.get_pods(namespace: actual_namespace).as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + 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 + def kubeclient @kubeclient ||= kubernetes_service.kubeclient if manages_kubernetes_service? end @@ -104,6 +185,48 @@ module Clusters def ensure_kubernetes_service @kubernetes_service ||= kubernetes_service || project&.build_kubernetes_service end + + def build_kubeclient!(api_path: 'api', api_version: 'v1') + raise "Incomplete settings" unless api_url && actual_namespace + + unless (username && password) || token + raise "Either username/password or token is required to access API" + end + + ::Kubeclient::Client.new( + join_api_url(api_path), + api_version, + auth_options: kubeclient_auth_options, + ssl_options: kubeclient_ssl_options, + http_proxy_uri: ENV['http_proxy'] + ) + end + + def kubeclient_auth_options + return { username: username, password: password } if username && password + return { bearer_token: token } if token + 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 terminal_auth + { + token: token, + ca_pem: ca_pem, + max_session_time: current_application_settings.terminal_max_session_time + } + end + + def enforce_namespace_to_lower_case + self.namespace = self.namespace&.downcase + end end end end diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index ed76be703a5..4ad6e214e2a 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -5,6 +5,8 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching 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 @@ -185,4 +187,137 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching 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, :configured) } + 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, :configured) } + 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 |