# frozen_string_literal: true require 'spec_helper' RSpec.describe Gitlab::Kubernetes::KubeClient do include StubRequests include KubernetesHelpers let(:api_url) { 'https://kubernetes.example.com/prefix' } let(:kubeclient_options) { { auth_options: { bearer_token: 'xyz' } } } let(:client) { described_class.new(api_url, **kubeclient_options) } before do stub_kubeclient_discover(api_url) end def method_call(client, method_name) case method_name when /\A(get_|delete_)/ client.public_send(method_name) when /\A(create_|update_|patch_)/ client.public_send(method_name, {}) else raise "Unknown method name #{method_name}" end end shared_examples 'a Kubeclient' do it 'is a Kubeclient::Client' do is_expected.to be_an_instance_of Kubeclient::Client end it 'has the kubeclient options' do expect(subject.auth_options).to eq({ bearer_token: 'xyz' }) end end shared_examples 'redirection not allowed' do |method_name| context 'api_url is redirected' do before do redirect_url = 'https://not-under-our-control.example.com/api/v1/pods' stub_request(:get, %r{\A#{api_url}/}) .to_return(status: 302, headers: { location: redirect_url }) stub_request(:get, redirect_url) .to_return(status: 200, body: '{}') end it 'does not follow redirects' do expect { method_call(client, method_name) }.to raise_error(Kubeclient::HttpError) end end end shared_examples 'dns rebinding not allowed' do |method_name| it 'does not allow DNS rebinding' do stub_dns(api_url, ip_address: '8.8.8.8') client stub_dns(api_url, ip_address: '192.168.2.120') expect { method_call(client, method_name) }.to raise_error(ArgumentError, /is blocked/) end end describe '.graceful_request' do context 'successful' do before do allow(client).to receive(:foo).and_return(true) end it 'returns connected status and foo response' do result = described_class.graceful_request(1) { client.foo } expect(result).to eq({ status: :connected, response: true }) end end context 'errored' do using RSpec::Parameterized::TableSyntax where(:error, :connection_status, :error_status) do SocketError | :unreachable | :connection_error OpenSSL::X509::CertificateError | :authentication_failure | :authentication_error StandardError | :unknown_failure | :unknown_error Kubeclient::HttpError.new(408, "timed out", nil) | :unreachable | :http_error Kubeclient::HttpError.new(408, "timeout", nil) | :unreachable | :http_error Kubeclient::HttpError.new(408, "", nil) | :authentication_failure | :http_error end with_them do before do allow(client).to receive(:foo).and_raise(error) end it 'returns error status' do result = described_class.graceful_request(1) { client.foo } expect(result).to eq({ status: connection_status, connection_error: error_status }) end end end end describe '#initialize' do shared_examples 'local address' do it 'blocks local addresses' do expect { client }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) end context 'when local requests are allowed' do before do stub_application_setting(allow_local_requests_from_web_hooks_and_services: true) end it 'allows local addresses' do expect { client }.not_to raise_error end end end context 'localhost address' do let(:api_url) { 'http://localhost:22' } it_behaves_like 'local address' end context 'private network address' do let(:api_url) { 'http://192.168.1.2:3003' } it_behaves_like 'local address' end it 'falls back to default options, but allows overriding' do client = described_class.new(api_url) defaults = Gitlab::Kubernetes::KubeClient::DEFAULT_KUBECLIENT_OPTIONS expect(client.kubeclient_options[:timeouts]).to eq(defaults[:timeouts]) client = Gitlab::Kubernetes::KubeClient.new(api_url, timeouts: { read: 7 }) expect(client.kubeclient_options[:timeouts][:read]).to eq(7) expect(client.kubeclient_options[:timeouts][:open]).to eq(defaults[:timeouts][:open]) end end describe '#core_client' do subject { client.core_client } it_behaves_like 'a Kubeclient' it 'has the core API endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/api\Z}) end it 'has the api_version' do expect(subject.instance_variable_get(:@api_version)).to eq('v1') end end describe '#rbac_client' do subject { client.rbac_client } it_behaves_like 'a Kubeclient' it 'has the RBAC API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/rbac.authorization.k8s.io\Z}) end it 'has the api_version' do expect(subject.instance_variable_get(:@api_version)).to eq('v1') end end describe '#extensions_client' do subject { client.extensions_client } it_behaves_like 'a Kubeclient' it 'has the extensions API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/extensions\Z}) end it 'has the api_version' do expect(subject.instance_variable_get(:@api_version)).to eq('v1beta1') end end describe '#istio_client' do subject { client.istio_client } it_behaves_like 'a Kubeclient' it 'has the Istio API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/networking.istio.io\Z}) end it 'has the api_version' do expect(subject.instance_variable_get(:@api_version)).to eq('v1alpha3') end end describe '#knative_client' do subject { client.knative_client } it_behaves_like 'a Kubeclient' it 'has the extensions API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/serving.knative.dev\Z}) end it 'has the api_version' do expect(subject.instance_variable_get(:@api_version)).to eq('v1alpha1') end end describe '#networking_client' do subject { client.networking_client } it_behaves_like 'a Kubeclient' it 'has the networking API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/networking.k8s.io\Z}) end it 'has the api_version' do expect(subject.instance_variable_get(:@api_version)).to eq('v1') end end describe '#cilium_networking_client' do subject { client.cilium_networking_client } it_behaves_like 'a Kubeclient' it 'has the cilium API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/cilium.io\Z}) end it 'has the api_version' do expect(subject.instance_variable_get(:@api_version)).to eq('v2') end end describe '#metrics_client' do subject { client.metrics_client } it_behaves_like 'a Kubeclient' it 'has the metrics API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/metrics.k8s.io\Z}) end it 'has the api_version' do expect(subject.instance_variable_get(:@api_version)).to eq('v1beta1') end end describe 'core API' do let(:core_client) { client.core_client } [ :get_nodes, :get_pods, :get_secrets, :get_config_map, :get_pod, :get_namespace, :get_secret, :get_service, :get_service_account, :delete_namespace, :delete_pod, :delete_service_account, :create_config_map, :create_namespace, :create_pod, :create_secret, :create_service_account, :update_config_map, :update_secret, :update_service_account ].each do |method| describe "##{method}" do include_examples 'redirection not allowed', method include_examples 'dns rebinding not allowed', method it 'delegates to the core client' do expect(client).to delegate_method(method).to(:core_client) end it 'responds to the method' do expect(client).to respond_to method end end end end describe 'rbac API group' do let(:rbac_client) { client.rbac_client } [ :create_role, :get_role, :update_role, :delete_role_binding, :update_role_binding, :update_cluster_role_binding ].each do |method| describe "##{method}" do include_examples 'redirection not allowed', method include_examples 'dns rebinding not allowed', method it 'delegates to the rbac client' do expect(client).to delegate_method(method).to(:rbac_client) end it 'responds to the method' do expect(client).to respond_to method end end end end describe '#get_deployments' do let(:extensions_client) { client.extensions_client } let(:apps_client) { client.apps_client } include_examples 'redirection not allowed', 'get_deployments' include_examples 'dns rebinding not allowed', 'get_deployments' it 'delegates to the extensions client' do expect(extensions_client).to receive(:get_deployments) client.get_deployments end context 'extensions does not have deployments for Kubernetes 1.16+ clusters' do before do WebMock .stub_request(:get, api_url + '/apis/extensions/v1beta1') .to_return(kube_response(kube_1_16_extensions_v1beta1_discovery_body)) end it 'delegates to the apps client' do expect(apps_client).to receive(:get_deployments) client.get_deployments end end end describe '#get_ingresses' do let(:extensions_client) { client.extensions_client } let(:networking_client) { client.networking_client } include_examples 'redirection not allowed', 'get_ingresses' include_examples 'dns rebinding not allowed', 'get_ingresses' it 'delegates to the extensions client' do expect(extensions_client).to receive(:get_ingresses) client.get_ingresses end context 'extensions does not have deployments for Kubernetes 1.22+ clusters' do before do WebMock .stub_request(:get, api_url + '/apis/extensions/v1beta1') .to_return(kube_response(kube_1_22_extensions_v1beta1_discovery_body)) end it 'delegates to the apps client' do expect(networking_client).to receive(:get_ingresses) client.get_ingresses end end end describe '#patch_ingress' do let(:extensions_client) { client.extensions_client } let(:networking_client) { client.networking_client } include_examples 'redirection not allowed', 'patch_ingress' include_examples 'dns rebinding not allowed', 'patch_ingress' it 'delegates to the extensions client' do expect(extensions_client).to receive(:patch_ingress) client.patch_ingress end context 'extensions does not have ingress for Kubernetes 1.22+ clusters' do before do WebMock .stub_request(:get, api_url + '/apis/extensions/v1beta1') .to_return(kube_response(kube_1_22_extensions_v1beta1_discovery_body)) end it 'delegates to the apps client' do expect(networking_client).to receive(:patch_ingress) client.patch_ingress end end end describe 'istio API group' do let(:istio_client) { client.istio_client } [ :create_gateway, :get_gateway, :update_gateway ].each do |method| describe "##{method}" do include_examples 'redirection not allowed', method include_examples 'dns rebinding not allowed', method it 'delegates to the istio client' do expect(client).to delegate_method(method).to(:istio_client) end it 'responds to the method' do expect(client).to respond_to method end end end end describe 'networking API group' do let(:networking_client) { client.networking_client } [ :create_network_policy, :get_network_policies, :get_network_policy, :update_network_policy, :delete_network_policy ].each do |method| describe "##{method}" do include_examples 'redirection not allowed', method include_examples 'dns rebinding not allowed', method it 'delegates to the networking client' do expect(client).to delegate_method(method).to(:networking_client) end it 'responds to the method' do expect(client).to respond_to method end end end end describe 'cilium API group' do let(:cilium_networking_client) { client.cilium_networking_client } [ :create_cilium_network_policy, :get_cilium_network_policies, :get_cilium_network_policy, :update_cilium_network_policy, :delete_cilium_network_policy ].each do |method| describe "##{method}" do include_examples 'redirection not allowed', method include_examples 'dns rebinding not allowed', method it 'delegates to the cilium client' do expect(client).to delegate_method(method).to(:cilium_networking_client) end it 'responds to the method' do expect(client).to respond_to method end end end end describe 'non-entity methods' do it 'does not proxy for non-entity methods' do expect(client).not_to respond_to :proxy_url end it 'throws an error' do expect { client.proxy_url }.to raise_error(NoMethodError) end end describe '#get_pod_log' do let(:core_client) { client.core_client } it 'is delegated to the core client' do expect(client).to delegate_method(:get_pod_log).to(:core_client) end end describe '#watch_pod_log' do let(:core_client) { client.core_client } it 'is delegated to the core client' do expect(client).to delegate_method(:watch_pod_log).to(:core_client) end end shared_examples 'create_or_update method using put' do let(:update_method) { "update_#{resource_type}" } it 'calls the update method' do expect(client).to receive(update_method).with(resource) subject end end shared_examples 'create_or_update method' do let(:get_method) { "get_#{resource_type}" } let(:update_method) { "update_#{resource_type}" } let(:create_method) { "create_#{resource_type}" } context 'resource exists' do before do expect(client).to receive(get_method).and_return(resource) end it 'calls the update method' do expect(client).to receive(update_method).with(resource) subject end end context 'resource does not exist' do before do expect(client).to receive(get_method).and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil)) end it 'calls the create method' do expect(client).to receive(create_method).with(resource) subject end end end describe '#create_or_update_cluster_role_binding' do let(:resource_type) { 'cluster_role_binding' } let(:resource) do ::Kubeclient::Resource.new(metadata: { name: 'name', namespace: 'namespace' }) end subject { client.create_or_update_cluster_role_binding(resource) } it_behaves_like 'create_or_update method using put' end describe '#create_or_update_role_binding' do let(:resource_type) { 'role_binding' } let(:resource) do ::Kubeclient::Resource.new(metadata: { name: 'name', namespace: 'namespace' }) end subject { client.create_or_update_role_binding(resource) } it_behaves_like 'create_or_update method using put' end describe '#create_or_update_service_account' do let(:resource_type) { 'service_account' } let(:resource) do ::Kubeclient::Resource.new(metadata: { name: 'name', namespace: 'namespace' }) end subject { client.create_or_update_service_account(resource) } it_behaves_like 'create_or_update method' end describe '#create_or_update_secret' do let(:resource_type) { 'secret' } let(:resource) do ::Kubeclient::Resource.new(metadata: { name: 'name', namespace: 'namespace' }) end subject { client.create_or_update_secret(resource) } it_behaves_like 'create_or_update method' end describe 'methods that do not exist on any client' do it 'throws an error' do expect { client.non_existent_method }.to raise_error(NoMethodError) end it 'returns false for respond_to' do expect(client.respond_to?(:non_existent_method)).to be_falsey end end end