"Fossies" - the Fresh Open Source Software Archive

Member "buildbot-2.5.1/buildbot/test/unit/test_util_kubeclientservice.py" (24 Nov 2019, 14986 Bytes) of package /linux/misc/buildbot-2.5.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "test_util_kubeclientservice.py": 2.3.1_vs_2.4.0.

    1 # This file is part of Buildbot.  Buildbot is free software: you can
    2 # redistribute it and/or modify it under the terms of the GNU General Public
    3 # License as published by the Free Software Foundation, version 2.
    4 #
    5 # This program is distributed in the hope that it will be useful, but WITHOUT
    6 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    7 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    8 # details.
    9 #
   10 # You should have received a copy of the GNU General Public License along with
   11 # this program; if not, write to the Free Software Foundation, Inc., 51
   12 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
   13 #
   14 # Copyright Buildbot Team Members
   15 
   16 import base64
   17 import copy
   18 import os
   19 import sys
   20 import textwrap
   21 from io import StringIO
   22 from unittest.case import SkipTest
   23 
   24 import yaml
   25 
   26 import mock
   27 
   28 from twisted.internet import defer
   29 from twisted.python import runtime
   30 from twisted.trial import unittest
   31 
   32 from buildbot.process.properties import Interpolate
   33 from buildbot.test.fake import fakemaster
   34 from buildbot.test.fake import httpclientservice as fakehttp
   35 from buildbot.test.fake import kube as fakekube
   36 from buildbot.test.util import config
   37 from buildbot.test.util.misc import TestReactorMixin
   38 from buildbot.util import kubeclientservice
   39 
   40 
   41 class MockFileBase:
   42     file_mock_config = {}
   43 
   44     def setUp(self):
   45         self.patcher = mock.patch('buildbot.util.kubeclientservice.open',
   46                                   self.mock_open)
   47         self.patcher.start()
   48 
   49     def tearDown(self):
   50         self.patcher.stop()
   51 
   52     def mock_open(self, filename, mode=None, encoding='UTF-8'):
   53         filename_type = os.path.basename(filename)
   54         file_value = self.file_mock_config[filename_type]
   55         mock_open = mock.Mock(
   56             __enter__=mock.Mock(return_value=StringIO(file_value)),
   57             __exit__=mock.Mock())
   58         return mock_open
   59 
   60 
   61 class KubeClientServiceTestClusterConfig(
   62         MockFileBase, config.ConfigErrorsMixin, unittest.SynchronousTestCase):
   63 
   64     file_mock_config = {
   65         'token': 'BASE64_TOKEN',
   66         'namespace': 'buildbot_namespace'
   67     }
   68 
   69     def setUp(self):
   70         super().setUp()
   71         self.patch(kubeclientservice.os, 'environ',
   72                    {'KUBERNETES_PORT': 'tcp://foo'})
   73 
   74     def patchExist(self, val):
   75         self.patch(kubeclientservice.os.path, 'exists', lambda x: val)
   76 
   77     def test_not_exists(self):
   78         self.patchExist(False)
   79         with self.assertRaisesConfigError('kube_dir not found:'):
   80             kubeclientservice.KubeInClusterConfigLoader()
   81 
   82     def test_basic(self):
   83         self.patchExist(True)
   84         config = kubeclientservice.KubeInClusterConfigLoader()
   85         self.successResultOf(config.startService())
   86         self.assertEqual(
   87             config.getConfig(), {
   88                 'headers': {
   89                     'Authorization': 'Bearer BASE64_TOKEN'
   90                 },
   91                 'master_url': 'https://foo',
   92                 'namespace': 'buildbot_namespace',
   93                 'verify':
   94                 '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
   95             })
   96 
   97 
   98 KUBE_CTL_PROXY_FAKE = """
   99 import time
  100 import sys
  101 
  102 print("Starting to serve on 127.0.0.1:" + sys.argv[2])
  103 sys.stdout.flush()
  104 time.sleep(1000)
  105 """
  106 
  107 KUBE_CTL_PROXY_FAKE_ERROR = """
  108 import time
  109 import sys
  110 
  111 print("Issue with the config!", file=sys.stderr)
  112 sys.stderr.flush()
  113 sys.exit(1)
  114 """
  115 
  116 
  117 class KubeClientServiceTestKubeHardcodedConfig(config.ConfigErrorsMixin,
  118                                               unittest.TestCase):
  119     def test_basic(self):
  120         self.config = config = kubeclientservice.KubeHardcodedConfig(
  121             master_url="http://localhost:8001",
  122             namespace="default"
  123         )
  124         self.assertEqual(config.getConfig(), {
  125             'master_url': 'http://localhost:8001',
  126             'namespace': 'default',
  127             'headers': {}
  128         })
  129 
  130     @defer.inlineCallbacks
  131     def test_verify_is_forwarded_to_keywords(self):
  132         self.config = config = kubeclientservice.KubeHardcodedConfig(
  133             master_url="http://localhost:8001",
  134             namespace="default",
  135             verify="/path/to/pem"
  136         )
  137         service = kubeclientservice.KubeClientService(config)
  138         url, kwargs = yield service._prepareRequest("/test", {})
  139         self.assertEqual('/path/to/pem', kwargs['verify'])
  140 
  141     @defer.inlineCallbacks
  142     def test_verify_headers_are_passed_to_the_query(self):
  143         self.config = config = kubeclientservice.KubeHardcodedConfig(
  144             master_url="http://localhost:8001",
  145             namespace="default",
  146             verify="/path/to/pem",
  147             headers={'Test': '10'}
  148         )
  149         service = kubeclientservice.KubeClientService(config)
  150         url, kwargs = yield service._prepareRequest("/test", {})
  151         self.assertEqual({'Test': '10'}, kwargs['headers'])
  152 
  153     def test_the_configuration_parent_is_set_to_the_service(self):
  154         # This is needed to allow secret expansion
  155         self.config = config = kubeclientservice.KubeHardcodedConfig(
  156             master_url="http://localhost:8001")
  157         service = kubeclientservice.KubeClientService(config)
  158         self.assertEqual(service, self.config.parent)
  159 
  160     def test_cannot_pass_both_bearer_and_basic_auth(self):
  161         with self.assertRaises(Exception):
  162             kubeclientservice.KubeHardcodedConfig(
  163                 master_url="http://localhost:8001",
  164                 namespace="default",
  165                 verify="/path/to/pem",
  166                 basicAuth="Bla",
  167                 bearerToken="Bla")
  168 
  169     @defer.inlineCallbacks
  170     def test_verify_bearerToken_is_expanded(self):
  171         self.config = config = kubeclientservice.KubeHardcodedConfig(
  172             master_url="http://localhost:8001",
  173             namespace="default",
  174             verify="/path/to/pem",
  175             bearerToken=Interpolate("%(kw:test)s", test=10))
  176         service = kubeclientservice.KubeClientService(config)
  177         url, kwargs = yield service._prepareRequest("/test", {})
  178         self.assertEqual("Bearer 10", kwargs['headers']['Authorization'])
  179 
  180     @defer.inlineCallbacks
  181     def test_verify_basicAuth_is_expanded(self):
  182         self.config = config = kubeclientservice.KubeHardcodedConfig(
  183             master_url="http://localhost:8001",
  184             namespace="default",
  185             verify="/path/to/pem",
  186             basicAuth={'user': 'name', 'password': Interpolate("%(kw:test)s", test=10)})
  187         service = kubeclientservice.KubeClientService(config)
  188         url, kwargs = yield service._prepareRequest("/test", {})
  189 
  190         expected = "Basic {0}".format(base64.b64encode("name:10".encode('utf-8')))
  191         self.assertEqual(expected, kwargs['headers']['Authorization'])
  192 
  193 
  194 class KubeClientServiceTestKubeCtlProxyConfig(config.ConfigErrorsMixin,
  195                                               unittest.TestCase):
  196     def patchProxyCmd(self, cmd):
  197         if runtime.platformType != 'posix':
  198             self.config = None
  199             raise SkipTest('only posix platform is supported by this test')
  200         self.patch(kubeclientservice.KubeCtlProxyConfigLoader,
  201                    'kube_ctl_proxy_cmd', [sys.executable, "-c", cmd])
  202 
  203     def tearDown(self):
  204         if self.config is not None:
  205             return self.config.stopService()
  206 
  207     @defer.inlineCallbacks
  208     def test_basic(self):
  209         self.patchProxyCmd(KUBE_CTL_PROXY_FAKE)
  210         self.config = config = kubeclientservice.KubeCtlProxyConfigLoader()
  211         yield config.startService()
  212         self.assertEqual(config.getConfig(), {
  213             'master_url': 'http://localhost:8001',
  214             'namespace': 'default'
  215         })
  216 
  217     @defer.inlineCallbacks
  218     def test_config_args(self):
  219         self.patchProxyCmd(KUBE_CTL_PROXY_FAKE)
  220         self.config = config = kubeclientservice.KubeCtlProxyConfigLoader(
  221             proxy_port=8002, namespace="system")
  222         yield config.startService()
  223         self.assertEqual(config.kube_proxy_output,
  224                          b'Starting to serve on 127.0.0.1:8002')
  225         self.assertEqual(config.getConfig(), {
  226             'master_url': 'http://localhost:8002',
  227             'namespace': 'system'
  228         })
  229         yield config.stopService()
  230 
  231     @defer.inlineCallbacks
  232     def test_config_with_error(self):
  233         self.patchProxyCmd(KUBE_CTL_PROXY_FAKE_ERROR)
  234         self.config = config = kubeclientservice.KubeCtlProxyConfigLoader()
  235         with self.assertRaises(RuntimeError):
  236             yield config.startService()
  237 
  238 
  239 # integration tests for KubeClientService
  240 class RealKubeClientServiceTest(TestReactorMixin, unittest.TestCase):
  241     timeout = 200
  242     POD_SPEC = yaml.safe_load(
  243         textwrap.dedent("""
  244     apiVersion: v1
  245     kind: Pod
  246     metadata:
  247         name: pod-example
  248     spec:
  249         containers:
  250         - name: alpine
  251           image: alpine
  252           command: ["sleep"]
  253           args: ["100"]
  254     """))
  255 
  256     def createKube(self):
  257         if "TEST_KUBERNETES" not in os.environ:
  258             raise SkipTest(
  259                 "kubernetes integration tests only run when environment "
  260                 "variable TEST_KUBERNETES is set")
  261 
  262         self.kube = kubeclientservice.KubeClientService(
  263             kubeclientservice.KubeCtlProxyConfigLoader())
  264 
  265     def expect(self, *args, **kwargs):
  266         pass
  267 
  268     def setUp(self):
  269         self.setUpTestReactor()
  270         self.master = fakemaster.make_master(self)
  271         self.createKube()
  272         self.kube.setServiceParent(self.master)
  273         return self.master.startService()
  274 
  275     def tearDown(self):
  276         return self.master.stopService()
  277 
  278     kube = None
  279 
  280     @defer.inlineCallbacks
  281     def test_create_and_delete_pod(self):
  282         content = {'kind': 'Pod', 'metadata': {'name': 'pod-example'}}
  283         self.expect(
  284             method='post',
  285             ep='/api/v1/namespaces/default/pods',
  286             params=None,
  287             data=None,
  288             json={
  289                 'apiVersion': 'v1',
  290                 'kind': 'Pod',
  291                 'metadata': {
  292                     'name': 'pod-example'
  293                 },
  294                 'spec': {
  295                     'containers': [{
  296                         'name': 'alpine',
  297                         'image': 'alpine',
  298                         'command': ['sleep'],
  299                         'args': ['100']
  300                     }]
  301                 }
  302             },
  303             content_json=content)
  304         res = yield self.kube.createPod(self.kube.namespace, self.POD_SPEC)
  305         self.assertEqual(res['kind'], 'Pod')
  306         self.assertEqual(res['metadata']['name'], 'pod-example')
  307         self.assertNotIn('deletionTimestamp', res['metadata'])
  308 
  309         content['metadata']['deletionTimestamp'] = 'now'
  310         self.expect(
  311             method='delete',
  312             ep='/api/v1/namespaces/default/pods/pod-example',
  313             params={'graceperiod': 0},
  314             data=None,
  315             json=None,
  316             code=200,
  317             content_json=content)
  318 
  319         res = yield self.kube.deletePod(self.kube.namespace, 'pod-example')
  320         self.assertEqual(res['kind'], 'Pod')
  321         self.assertIn('deletionTimestamp', res['metadata'])
  322 
  323         # first time present
  324         self.expect(
  325             method='get',
  326             ep='/api/v1/namespaces/default/pods/pod-example/status',
  327             params=None,
  328             data=None,
  329             json=None,
  330             code=200,
  331             content_json=content)
  332         # second time deleted
  333         content = {'kind': 'Status', 'reason': 'NotFound'}
  334         self.expect(
  335             method='get',
  336             ep='/api/v1/namespaces/default/pods/pod-example/status',
  337             params=None,
  338             data=None,
  339             json=None,
  340             code=404,
  341             content_json=content)
  342 
  343         res = yield self.kube.waitForPodDeletion(
  344             self.kube.namespace, 'pod-example', timeout=200)
  345         self.assertEqual(res['kind'], 'Status')
  346         self.assertEqual(res['reason'], 'NotFound')
  347 
  348     @defer.inlineCallbacks
  349     def test_create_bad_spec(self):
  350         spec = copy.deepcopy(self.POD_SPEC)
  351         del spec['metadata']
  352         content = {
  353             'kind': 'Status',
  354             'reason': 'MissingName',
  355             'message': 'need name'
  356         }
  357         self.expect(
  358             method='post',
  359             ep='/api/v1/namespaces/default/pods',
  360             params=None,
  361             data=None,
  362             json={
  363                 'apiVersion': 'v1',
  364                 'kind': 'Pod',
  365                 'spec': {
  366                     'containers': [{
  367                         'name': 'alpine',
  368                         'image': 'alpine',
  369                         'command': ['sleep'],
  370                         'args': ['100']
  371                     }]
  372                 }
  373             },
  374             code=400,
  375             content_json=content)
  376         with self.assertRaises(kubeclientservice.KubeError):
  377             yield self.kube.createPod(self.kube.namespace, spec)
  378 
  379     @defer.inlineCallbacks
  380     def test_delete_not_existing(self):
  381         content = {
  382             'kind': 'Status',
  383             'reason': 'NotFound',
  384             'message': 'no container by that name'
  385         }
  386         self.expect(
  387             method='delete',
  388             ep='/api/v1/namespaces/default/pods/pod-example',
  389             params={'graceperiod': 0},
  390             data=None,
  391             json=None,
  392             code=404,
  393             content_json=content)
  394         with self.assertRaises(kubeclientservice.KubeError):
  395             yield self.kube.deletePod(self.kube.namespace, 'pod-example')
  396 
  397     @defer.inlineCallbacks
  398     def test_wait_for_delete_not_deleting(self):
  399         yield self.kube.createPod(self.kube.namespace, self.POD_SPEC)
  400         with self.assertRaises(TimeoutError):
  401             yield self.kube.waitForPodDeletion(
  402                 self.kube.namespace, 'pod-example', timeout=2)
  403         res = yield self.kube.deletePod(self.kube.namespace, 'pod-example')
  404         self.assertEqual(res['kind'], 'Pod')
  405         self.assertIn('deletionTimestamp', res['metadata'])
  406         yield self.kube.waitForPodDeletion(
  407             self.kube.namespace, 'pod-example', timeout=100)
  408 
  409 
  410 class FakeKubeClientServiceTest(RealKubeClientServiceTest):
  411     def createKube(self):
  412         self.kube = fakekube.KubeClientService(
  413             kubeclientservice.KubeHardcodedConfig(master_url='http://m'))
  414 
  415 
  416 class PatchedKubeClientServiceTest(RealKubeClientServiceTest):
  417     def createKube(self):
  418         self.kube = kubeclientservice.KubeClientService(
  419             kubeclientservice.KubeHardcodedConfig(master_url='http://m'))
  420         self.http = fakehttp.HTTPClientService('http://m')
  421         self.kube.get = self.http.get
  422         self.kube.post = self.http.post
  423         self.kube.put = self.http.put
  424         self.kube.delete = self.http.delete
  425 
  426     def expect(self, *args, **kwargs):
  427         return self.http.expect(*args, **kwargs)
  428 
  429     def test_wait_for_delete_not_deleting(self):
  430         # no need to describe the expect flow for that case
  431         pass