Commit fc6e3515 authored by Nick Thomas's avatar Nick Thomas

Backport EE changes to the Kubernetes service

parent fbfddd99
...@@ -116,30 +116,19 @@ class KubernetesService < DeploymentService ...@@ -116,30 +116,19 @@ class KubernetesService < DeploymentService
# short time later # short time later
def terminals(environment) def terminals(environment)
with_reactive_cache do |data| with_reactive_cache do |data|
pods = data.fetch(:pods, nil) pods = filter_by_label(data[:pods], app: environment.slug)
filter_pods(pods, app: environment.slug). terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }
flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }. terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end end
end end
# Caches all pods in the namespace so other calls don't need to block on # Caches resources in the namespace so other calls don't need to block on
# network access. # network access
def calculate_reactive_cache def calculate_reactive_cache
return unless active? && project && !project.pending_delete? return unless active? && project && !project.pending_delete?
kubeclient = build_kubeclient!
# Store as hashes, rather than as third-party types
pods = begin
kubeclient.get_pods(namespace: actual_namespace).as_json
rescue KubeException => err
raise err unless err.error_code == 404
[]
end
# We may want to cache extra things in the future # We may want to cache extra things in the future
{ pods: pods } { pods: read_pods }
end end
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
...@@ -166,6 +155,16 @@ class KubernetesService < DeploymentService ...@@ -166,6 +155,16 @@ class KubernetesService < DeploymentService
) )
end 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 def kubeclient_ssl_options
opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
...@@ -181,11 +180,11 @@ class KubernetesService < DeploymentService ...@@ -181,11 +180,11 @@ class KubernetesService < DeploymentService
{ bearer_token: token } { bearer_token: token }
end end
def join_api_url(*parts) def join_api_url(api_path)
url = URI.parse(api_url) url = URI.parse(api_url)
prefix = url.path.sub(%r{/+\z}, '') prefix = url.path.sub(%r{/+\z}, '')
url.path = [prefix, *parts].join("/") url.path = [prefix, api_path].join("/")
url.to_s url.to_s
end end
......
...@@ -8,13 +8,13 @@ module Gitlab ...@@ -8,13 +8,13 @@ module Gitlab
) )
# Filters an array of pods (as returned by the kubernetes API) by their labels # Filters an array of pods (as returned by the kubernetes API) by their labels
def filter_pods(pods, labels = {}) def filter_by_label(items, labels = {})
pods.select do |pod| items.select do |item|
metadata = pod.fetch("metadata", {}) metadata = item.fetch("metadata", {})
pod_labels = metadata.fetch("labels", nil) item_labels = metadata.fetch("labels", nil)
next unless pod_labels next unless item_labels
labels.all? { |k, v| pod_labels[k.to_s] == v } labels.all? { |k, v| item_labels[k.to_s] == v }
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Kubernetes do describe Gitlab::Kubernetes do
include KubernetesHelpers
include described_class include described_class
describe '#container_exec_url' do describe '#container_exec_url' do
...@@ -36,4 +37,13 @@ describe Gitlab::Kubernetes do ...@@ -36,4 +37,13 @@ describe Gitlab::Kubernetes do
it { expect(result.query).to match(/\Acontainer=container\+1&/) } it { expect(result.query).to match(/\Acontainer=container\+1&/) }
end end
end end
describe '#filter_by_label' do
it 'returns matching labels' do
matching_items = [kube_pod(app: 'foo')]
items = matching_items + [kube_pod]
expect(filter_by_label(items, app: 'foo')).to eq(matching_items)
end
end
end end
...@@ -7,24 +7,6 @@ describe KubernetesService, models: true, caching: true do ...@@ -7,24 +7,6 @@ describe KubernetesService, models: true, caching: true do
let(:project) { build_stubbed(:kubernetes_project) } let(:project) { build_stubbed(:kubernetes_project) }
let(:service) { project.kubernetes_service } let(:service) { project.kubernetes_service }
# We use Kubeclient to interactive with the Kubernetes API. It will
# GET /api/v1 for a list of resources the API supports. This must be stubbed
# in addition to any other HTTP requests we expect it to perform.
let(:discovery_url) { service.api_url + '/api/v1' }
let(:discovery_response) { { body: kube_discovery_body.to_json } }
let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods" }
let(:pods_response) { { body: kube_pods_body(kube_pod).to_json } }
def stub_kubeclient_discover
WebMock.stub_request(:get, discovery_url).to_return(discovery_response)
end
def stub_kubeclient_pods
stub_kubeclient_discover
WebMock.stub_request(:get, pods_url).to_return(pods_response)
end
describe "Associations" do describe "Associations" do
it { is_expected.to belong_to :project } it { is_expected.to belong_to :project }
end end
...@@ -105,6 +87,34 @@ describe KubernetesService, models: true, caching: true do ...@@ -105,6 +87,34 @@ describe KubernetesService, models: true, caching: true do
end end
end end
describe '#actual_namespace' do
subject { service.actual_namespace }
it "returns the default namespace" do
is_expected.to eq(service.send(:default_namespace))
end
context 'when namespace is specified' do
before do
service.namespace = 'my-namespace'
end
it "returns the user-namespace" do
is_expected.to eq('my-namespace')
end
end
context 'when service is not assigned to project' do
before do
service.project = nil
end
it "does not return namespace" do
is_expected.to be_nil
end
end
end
describe '#actual_namespace' do describe '#actual_namespace' do
subject { service.actual_namespace } subject { service.actual_namespace }
...@@ -134,6 +144,8 @@ describe KubernetesService, models: true, caching: true do ...@@ -134,6 +144,8 @@ describe KubernetesService, models: true, caching: true do
end end
describe '#test' do describe '#test' do
let(:discovery_url) { 'https://kubernetes.example.com/api/v1' }
before do before do
stub_kubeclient_discover stub_kubeclient_discover
end end
...@@ -142,7 +154,8 @@ describe KubernetesService, models: true, caching: true do ...@@ -142,7 +154,8 @@ describe KubernetesService, models: true, caching: true do
let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' } let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' }
it 'tests with the prefix' do it 'tests with the prefix' do
service.api_url = 'https://kubernetes.example.com/prefix/' service.api_url = 'https://kubernetes.example.com/prefix'
stub_kubeclient_discover
expect(service.test[:success]).to be_truthy expect(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once expect(WebMock).to have_requested(:get, discovery_url).once
...@@ -170,9 +183,9 @@ describe KubernetesService, models: true, caching: true do ...@@ -170,9 +183,9 @@ describe KubernetesService, models: true, caching: true do
end end
context 'failure' do context 'failure' do
let(:discovery_response) { { status: 404 } }
it 'fails to read the discovery endpoint' do it 'fails to read the discovery endpoint' do
WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(status: 404)
expect(service.test[:success]).to be_falsy expect(service.test[:success]).to be_falsy
expect(WebMock).to have_requested(:get, discovery_url).once expect(WebMock).to have_requested(:get, discovery_url).once
end end
...@@ -258,7 +271,6 @@ describe KubernetesService, models: true, caching: true do ...@@ -258,7 +271,6 @@ describe KubernetesService, models: true, caching: true do
end end
describe '#calculate_reactive_cache' do describe '#calculate_reactive_cache' do
before { stub_kubeclient_pods }
subject { service.calculate_reactive_cache } subject { service.calculate_reactive_cache }
context 'when service is inactive' do context 'when service is inactive' do
...@@ -268,17 +280,25 @@ describe KubernetesService, models: true, caching: true do ...@@ -268,17 +280,25 @@ describe KubernetesService, models: true, caching: true do
end end
context 'when kubernetes responds with valid pods' do context 'when kubernetes responds with valid pods' do
before do
stub_kubeclient_pods
end
it { is_expected.to eq(pods: [kube_pod]) } it { is_expected.to eq(pods: [kube_pod]) }
end end
context 'when kubernetes responds with 500' do context 'when kubernetes responds with 500s' do
let(:pods_response) { { status: 500 } } before do
stub_kubeclient_pods(status: 500)
end
it { expect { subject }.to raise_error(KubeException) } it { expect { subject }.to raise_error(KubeException) }
end end
context 'when kubernetes responds with 404' do context 'when kubernetes responds with 404s' do
let(:pods_response) { { status: 404 } } before do
stub_kubeclient_pods(status: 404)
end
it { is_expected.to eq(pods: []) } it { is_expected.to eq(pods: []) }
end end
......
module KubernetesHelpers module KubernetesHelpers
include Gitlab::Kubernetes include Gitlab::Kubernetes
def kube_discovery_body def kube_response(body)
{ body: body.to_json }
end
def kube_pods_response
kube_response(kube_pods_body)
end
def stub_kubeclient_discover
WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
end
def stub_kubeclient_pods(response = nil)
stub_kubeclient_discover
pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods"
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
def kube_v1_discovery_body
{ {
"kind" => "APIResourceList", "kind" => "APIResourceList",
"resources" => [ "resources" => [
...@@ -10,17 +29,19 @@ module KubernetesHelpers ...@@ -10,17 +29,19 @@ module KubernetesHelpers
} }
end end
def kube_pods_body(*pods) def kube_pods_body
{ "kind" => "PodList", {
"items" => [kube_pod] } "kind" => "PodList",
"items" => [kube_pod]
}
end end
# This is a partial response, it will have many more elements in reality but # This is a partial response, it will have many more elements in reality but
# these are the ones we care about at the moment # these are the ones we care about at the moment
def kube_pod(app: "valid-pod-label") def kube_pod(name: "kube-pod", app: "valid-pod-label")
{ {
"metadata" => { "metadata" => {
"name" => "kube-pod", "name" => name,
"creationTimestamp" => "2016-11-25T19:55:19Z", "creationTimestamp" => "2016-11-25T19:55:19Z",
"labels" => { "app" => app } "labels" => { "app" => app }
}, },
......
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