kubernetes_service_spec.rb 7.66 KB
Newer Older
1 2
require 'spec_helper'

3 4 5 6
describe KubernetesService, models: true, caching: true do
  include KubernetesHelpers
  include ReactiveCachingHelpers

7
  let(:project) { build_stubbed(:kubernetes_project) }
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
  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.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
27 28 29 30 31 32 33 34

  describe "Associations" do
    it { is_expected.to belong_to :project }
  end

  describe 'Validations' do
    context 'when service is active' do
      before { subject.active = true }
35 36

      it { is_expected.not_to validate_presence_of(:namespace) }
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
      it { is_expected.to validate_presence_of(:api_url) }
      it { is_expected.to validate_presence_of(:token) }

      context 'namespace format' do
        before do
          subject.project = project
          subject.api_url = "http://example.com"
          subject.token = "test"
        end

        {
          'foo'  => true,
          '1foo' => true,
          'foo1' => true,
          'foo-bar' => true,
          '-foo' => false,
          'foo-' => false,
          'a' * 63 => true,
          'a' * 64 => false,
          'a.b' => false,
57
          'a*b' => false
58
        }.each do |namespace, validity|
59
          it "validates #{namespace} as #{validity ? 'valid' : 'invalid'}" do
60 61 62 63 64 65 66 67 68 69
            subject.namespace = namespace

            expect(subject.valid?).to eq(validity)
          end
        end
      end
    end

    context 'when service is inactive' do
      before { subject.active = false }
70

71 72 73 74 75 76
      it { is_expected.not_to validate_presence_of(:api_url) }
      it { is_expected.not_to validate_presence_of(:token) }
    end
  end

  describe '#initialize_properties' do
77 78 79 80 81 82 83 84 85 86 87 88 89 90
    context 'without a project' do
      it 'leaves the namespace unset' do
        expect(described_class.new.namespace).to be_nil
      end
    end
  end

  describe '#fields' do
    let(:kube_namespace) do
      subject.fields.find { |h| h[:name] == 'namespace' }
    end

    context 'as template' do
      before { subject.template = true }
91

92 93 94
      it 'sets the namespace to the default' do
        expect(kube_namespace).not_to be_nil
        expect(kube_namespace[:placeholder]).to eq(subject.class::TEMPLATE_PLACEHOLDER)
95 96 97
      end
    end

98 99 100 101 102 103
    context 'with associated project' do
      before { subject.project = project }

      it 'sets the namespace to the default' do
        expect(kube_namespace).not_to be_nil
        expect(kube_namespace[:placeholder]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
104 105 106 107 108
      end
    end
  end

  describe '#test' do
109 110 111
    before do
      stub_kubeclient_discover
    end
112 113 114 115 116

    context 'with path prefix in api_url' do
      let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' }

      it 'tests with the prefix' do
117
        service.api_url = 'https://kubernetes.example.com/prefix/'
118 119 120 121 122 123 124 125

        expect(service.test[:success]).to be_truthy
        expect(WebMock).to have_requested(:get, discovery_url).once
      end
    end

    context 'with custom CA certificate' do
      it 'is added to the certificate store' do
126
        service.ca_pem = "CA PEM DATA"
127

128 129
        cert = double("certificate")
        expect(OpenSSL::X509::Certificate).to receive(:new).with(service.ca_pem).and_return(cert)
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
        expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert)

        expect(service.test[:success]).to be_truthy
        expect(WebMock).to have_requested(:get, discovery_url).once
      end
    end

    context 'success' do
      it 'reads the discovery endpoint' do
        expect(service.test[:success]).to be_truthy
        expect(WebMock).to have_requested(:get, discovery_url).once
      end
    end

    context 'failure' do
145
      let(:discovery_response) { { status: 404 } }
146

147
      it 'fails to read the discovery endpoint' do
148 149 150 151 152
        expect(service.test[:success]).to be_falsy
        expect(WebMock).to have_requested(:get, discovery_url).once
      end
    end
  end
153 154 155 156 157 158

  describe '#predefined_variables' do
    before do
      subject.api_url = 'https://kube.domain.com'
      subject.token = 'token'
      subject.ca_pem = 'CA PEM DATA'
159
      subject.project = project
160 161
    end

162 163
    context 'namespace is provided' do
      before { subject.namespace = 'my-project' }
164

165 166 167 168 169 170
      it 'sets the variables' do
        expect(subject.predefined_variables).to include(
          { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
          { key: 'KUBE_TOKEN', value: 'token', public: false },
          { key: 'KUBE_NAMESPACE', value: 'my-project', public: true },
          { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
171
          { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
172 173
        )
      end
174 175
    end

176 177 178 179 180 181
    context 'no namespace provided' do
      it 'sets the variables' do
        expect(subject.predefined_variables).to include(
          { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
          { key: 'KUBE_TOKEN', value: 'token', public: false },
          { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
182
          { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
183 184
        )
      end
185

186 187
      it 'sets the KUBE_NAMESPACE' do
        kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
188

189 190 191
        expect(kube_namespace).not_to be_nil
        expect(kube_namespace[:value]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
      end
192
    end
193
  end
194 195 196 197 198 199 200

  describe '#terminals' do
    let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
    subject { service.terminals(environment) }

    context 'with invalid pods' do
      it 'returns no terminals' do
201
        stub_reactive_cache(service, pods: [{ "bad" => "pod" }])
202 203 204 205 206 207 208 209 210

        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) }

211 212 213
      before do
        stub_reactive_cache(
          service,
214
          pods: [pod, pod, kube_pod(app: "should-be-filtered-out")]
215 216
        )
      end
217

218
      it 'returns terminals' do
219 220
        is_expected.to eq(terminals + terminals)
      end
221 222 223 224 225 226 227

      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
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
    end
  end

  describe '#calculate_reactive_cache' do
    before { stub_kubeclient_pods }
    subject { service.calculate_reactive_cache }

    context 'when service is inactive' do
      before { service.active = false }

      it { is_expected.to be_nil }
    end

    context 'when kubernetes responds with valid pods' do
      it { is_expected.to eq(pods: [kube_pod]) }
    end

    context 'when kubernetes responds with 500' do
      let(:pods_response) { { status: 500 } }

      it { expect { subject }.to raise_error(KubeException) }
    end

    context 'when kubernetes responds with 404' do
      let(:pods_response) { { status: 404 } }

      it { is_expected.to eq(pods: []) }
    end
  end
257
end