blob: dd0fea9908bbef586ff0c929e7ec315d42341e12 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
import io
import os
import subprocess
import unittest
from unittest.mock import MagicMock, patch
from openserverless import datastore_connection as connection
class PrivateServiceConnectionTests(unittest.TestCase):
def process(self, line='Forwarding from 127.0.0.1:41237 -> 2379\n'):
process = MagicMock()
process.stdout = io.StringIO(line)
process.poll.return_value = None
return process
def test_in_cluster_uses_service_without_starting_a_tunnel(self):
with patch.dict(os.environ, {'KUBERNETES_SERVICE_HOST': 'cluster'}, clear=True), \
patch.object(connection.subprocess, 'Popen') as spawn:
with connection.service_endpoint('openserverless-etcd', 2379) as endpoint:
self.assertEqual(endpoint, ('openserverless-etcd', 2379))
spawn.assert_not_called()
def test_external_tunnel_binds_only_loopback_and_closes(self):
process = self.process()
with patch.dict(os.environ, {}, clear=True), \
patch.object(connection.subprocess, 'Popen', return_value=process) as spawn, \
patch.object(connection.selectors, 'DefaultSelector') as selector:
selector.return_value.__enter__.return_value.select.return_value = [object()]
with connection.service_endpoint('openserverless-etcd', 2379) as endpoint:
self.assertEqual(endpoint, ('127.0.0.1', 41237))
command = spawn.call_args.args[0]
self.assertIn('--address=127.0.0.1', command)
self.assertIn('service/openserverless-etcd', command)
self.assertIn(':2379', command)
self.assertFalse(any('password' in item for item in command))
process.terminate.assert_called_once()
self.assertTrue(process.stdout.closed)
def test_consumer_error_still_closes_tunnel(self):
process = self.process()
with patch.dict(os.environ, {}, clear=True), \
patch.object(connection.subprocess, 'Popen', return_value=process), \
patch.object(connection.selectors, 'DefaultSelector') as selector:
selector.return_value.__enter__.return_value.select.return_value = [object()]
with self.assertRaisesRegex(ValueError, 'consumer failed'):
with connection.service_endpoint('openserverless-etcd', 2379):
raise ValueError('consumer failed')
process.terminate.assert_called_once()
self.assertTrue(process.stdout.closed)
def test_failed_tunnel_does_not_report_process_output(self):
process = self.process('credential-bearing diagnostic must stay private\n')
process.poll.return_value = 1
with patch.dict(os.environ, {}, clear=True), \
patch.object(connection.subprocess, 'Popen', return_value=process), \
patch.object(connection.selectors, 'DefaultSelector') as selector:
selector.return_value.__enter__.return_value.select.return_value = [object()]
with self.assertRaisesRegex(RuntimeError, '^Could not open the private datastore tunnel$'):
with connection.service_endpoint('openserverless-etcd', 2379):
self.fail('A failed tunnel must not yield an endpoint')
def test_a_stuck_tunnel_is_killed_during_cleanup(self):
process = self.process()
process.wait.side_effect = [subprocess.TimeoutExpired('kubectl', 5), None]
with patch.dict(os.environ, {}, clear=True), \
patch.object(connection.subprocess, 'Popen', return_value=process), \
patch.object(connection.selectors, 'DefaultSelector') as selector:
selector.return_value.__enter__.return_value.select.return_value = [object()]
with connection.service_endpoint('openserverless-etcd', 2379):
pass
process.kill.assert_called_once()
self.assertTrue(process.stdout.closed)
def test_invalid_targets_cannot_be_kubectl_options(self):
with patch.object(connection.subprocess, 'Popen') as spawn:
for service, port in [('--all-namespaces', 2379), ('service;command', 2379),
('redis', 0), ('redis', 65536)]:
with self.assertRaises(ValueError):
with connection.service_endpoint(service, port):
self.fail('Invalid target accepted')
spawn.assert_not_called()