You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@buildstream.apache.org by no...@apache.org on 2020/12/29 12:27:59 UTC

[buildstream] branch dp0/casserver-tests created (now e9ffd7e)

This is an automated email from the ASF dual-hosted git repository.

not-in-ldap pushed a change to branch dp0/casserver-tests
in repository https://gitbox.apache.org/repos/asf/buildstream.git.


      at e9ffd7e  Adapt tests to use secured CAS server

This branch includes the following new commits:

     new 5b476dd  Refactor casserver for better coverage
     new e9ffd7e  Adapt tests to use secured CAS server

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[buildstream] 02/02: Adapt tests to use secured CAS server

Posted by no...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

not-in-ldap pushed a commit to branch dp0/casserver-tests
in repository https://gitbox.apache.org/repos/asf/buildstream.git

commit e9ffd7eab8608d6a400182e2f1af8a8f97e3a7f9
Author: Daniel Playle <dp...@bloomberg.net>
AuthorDate: Fri Aug 31 16:55:24 2018 +0100

    Adapt tests to use secured CAS server
    
    Previously, we had been only testing an unsecured CAS server. As such,
    no authentication mechanisms were tested. This commit adapts existing
    tests to ensure that a secured CAS server is also tested alongside
    unsecured configurations.
    
    This commit introduces two data files which are used in part of the
    testing process: a server private key, and a server certificate. The
    server private key is of size 4096 bytes. The server certificate is for
    the name "localhost" and has an expiration set to 1000 years in the
    future, so there should be little concern of the expiration of this
    testing artifact.
---
 tests/frontend/creds/server_cert.pem | 29 +++++++++++++
 tests/frontend/creds/server_key.pem  | 52 ++++++++++++++++++++++
 tests/frontend/push.py               | 84 ++++++++++++++++++++++++++++--------
 tests/testutils/artifactshare.py     | 19 +++++---
 4 files changed, 160 insertions(+), 24 deletions(-)

diff --git a/tests/frontend/creds/server_cert.pem b/tests/frontend/creds/server_cert.pem
new file mode 100644
index 0000000..b1c6dc2
--- /dev/null
+++ b/tests/frontend/creds/server_cert.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIFADCCAuigAwIBAgIJAMrnbCKz2am/MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
+BAMMCWxvY2FsaG9zdDAgFw0xODA4MzExNTEzMThaGA8zMDE4MDEwMTE1MTMxOFow
+FDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAtOF+EmfIIQVz4eox6lZLcQcsf089r65XB5su8SzDXvC2kYIn8Crqs67Q
+x4s10YfYAOWS0Bj0Jx5GS7NQB+vUQtHWNTAO+19edAsrH204OD3QTrU0rt1rpJGb
+P36tIgZk+zgWj9MquHibacg2u/sz+4OlfxsDg2FDt1zhgamY9AQ2BRlxDza9/lcU
+/yBD2hSw3LyLQLJAL0TbTARqUkCHWZRcy1KPQ47SV4aWC2WhUwimDK44UZs1Ub50
+GcaB7s/ZRm6mREGV5mBzW63GLthOGlTps5YzE45GsLrmRiQ9aJgr8I6BZoNggLl9
+WomvGvd4PBzsXxyu3d+ZVdoudxeQbMKlyr7i5yVDO5S26xohUbpQg8wAl4YZ7tG7
+K2ihEphSgeCh3owWPkptXSxX+dXnA7W2/uJ4HwHjRLf7/MRL3GccJsviL7qtg9fO
+PF5av/psRkfR5tq/qtFrWAor4E1/nLKJbtEzHP8XoSJglSIXL2g1Q6ofNmbHvghg
+hR0pT6oEf6hc5R+qXVT7TTSxdVQkyLnrR+nUWMzaMUrD990kdhY+eJu15sJXsHyJ
+OThkuqiGbiTlTUEyhbTsFCK1UVi9P8cLGndyeB9LtQynWVqBFXPkT/qVyjOINUHZ
+2wu2U0OhjMpZ3dtQC58ME/Q0xDbJoyr+HwsMFV97TXT/WoLiN48CAwEAAaNTMFEw
+HQYDVR0OBBYEFFWfm5e2tS2Ask/QRNP8Y10yM3S4MB8GA1UdIwQYMBaAFFWfm5e2
+tS2Ask/QRNP8Y10yM3S4MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD
+ggIBACGpGGhliKiwAu1TjxMoQfI9rpO7uVyEYeBIYJ/eK3beJqCs3FmirgYdmGtK
+2KHcPnMG27c1m9EEsJf9sVOjkd43OB7fdWw38i0TaPxSgmPh0v6b+jhR4tqxwIRi
+jSQUxNklurKroyMWL9QFFLufwd43g3XTm449EOzrLB2jxUd10u+OXwzraQRRgTeM
+9U9PMoTfp6dICKKcV7XJdDR2hUH7SRrk96ucgYhztLx4x9R+mTZzgeCw5euY7a/t
+02uijT4tIrtCryOTukHrtfWdy5+ng4mcsdlWvZgJiRy+vwdWFx8k5t/MJ6f1xqxs
+RHl/99LAh/d9scdkVXPEB57vQRuHeybPH2i4cM/0VyFDfCrCG9AeXeVeB/pbanzB
+ex6MHttnhopTWtFHuMDquCeLP5P5cnKNLB676bZvKhgNoYZAXIrFGJtJNLMFBXD6
+v9kXrCIpDdUFHd3GLi8U9GTiwSmfz6HQVCvQZ+feVBpZLxaFrLpbRszKIE1lYFVS
+eJd3StxS/BGm3jzbWGgG9kq7kuF3cuJmtfKoAzOYYNz3/eipfQl6giOitJx5MWW5
+mevCq9mCNBIKvRNdxR0kJ4rJ5eTlDJ6xfFs2aKGXHFKS1+21RM/S58El+XosWXov
+4jZCTLS7wmk2/MrABGmbCgK4YfrQnt6eY2nElJXEBPX0jeHu
+-----END CERTIFICATE-----
diff --git a/tests/frontend/creds/server_key.pem b/tests/frontend/creds/server_key.pem
new file mode 100644
index 0000000..ba546dc
--- /dev/null
+++ b/tests/frontend/creds/server_key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+e+XVdfiURwWj6Q7/y/Rujtp7K6z2dA==
+-----END PRIVATE KEY-----
diff --git a/tests/frontend/push.py b/tests/frontend/push.py
index f351e33..74f0623 100644
--- a/tests/frontend/push.py
+++ b/tests/frontend/push.py
@@ -35,6 +35,22 @@ DATA_DIR = os.path.join(
     "project",
 )
 
+# Credential directory
+CRED_DIR = os.path.join(
+    os.path.dirname(os.path.realpath(__file__)),
+    "creds",
+)
+
+# Parameters for credentials
+CREDENTIAL_FILENAMES = {
+    'unsecured': {},
+
+    'server_secured': {
+        'server_key': 'server_key.pem',
+        'server_cert': 'server_cert.pem',
+    },
+}
+
 
 # Assert that a given artifact is in the share
 #
@@ -60,14 +76,41 @@ def assert_not_shared(cli, share, project, element_name):
                              .format(share.repo, element_name))
 
 
+# Taking a dictionary of filenames, this returns a dictionary of qualified
+# fielnames
+def join_credentials_path(credential_filenames, credential_files):
+    return {
+        key: os.path.join(credential_files, filename)
+        for key, filename in credential_filenames.items()
+    }
+
+
+# Adds the server certificate to the configuration if it exists and returns
+# this for ease of use
+def add_client_config_creds(configuration, credentials):
+    if 'server_cert' in credentials:
+        artifacts = configuration['artifacts']
+        if isinstance(artifacts, (list,)):
+            for subconfig in artifacts:
+                subconfig['server-cert'] = credentials['server_cert']
+        else:
+            artifacts['server-cert'] = credentials['server_cert']
+    return configuration
+
+
 # Tests that:
 #
 #  * `bst push` fails if there are no remotes configured for pushing
 #  * `bst push` successfully pushes to any remote that is configured for pushing
 #
-@pytest.mark.datafiles(DATA_DIR)
-def test_push(cli, tmpdir, datafiles):
-    project = str(datafiles)
+@pytest.mark.parametrize(
+    'credential_filenames', CREDENTIAL_FILENAMES.values(), ids=list(CREDENTIAL_FILENAMES))
+@pytest.mark.datafiles(DATA_DIR, CRED_DIR, keep_top_dir=True)
+def test_push(cli, tmpdir, datafiles, credential_filenames):
+    project = os.path.join(datafiles, 'project')
+    credfiles = os.path.join(datafiles, 'creds')
+
+    credentials = join_credentials_path(credential_filenames, credfiles)
 
     # First build the project without the artifact cache configured
     result = cli.run(project=project, args=['build', 'target.bst'])
@@ -77,9 +120,11 @@ def test_push(cli, tmpdir, datafiles):
     assert cli.get_element_state(project, 'target.bst') == 'cached'
 
     # Set up two artifact shares.
-    with create_artifact_share(os.path.join(str(tmpdir), 'artifactshare1')) as share1:
+    with create_artifact_share(os.path.join(str(tmpdir), 'artifactshare1'),
+                               credentials=credentials) as share1:
 
-        with create_artifact_share(os.path.join(str(tmpdir), 'artifactshare2')) as share2:
+        with create_artifact_share(os.path.join(str(tmpdir), 'artifactshare2'),
+                                   credentials=credentials) as share2:
 
             # Try pushing with no remotes configured. This should fail.
             result = cli.run(project=project, args=['push', 'target.bst'])
@@ -87,19 +132,19 @@ def test_push(cli, tmpdir, datafiles):
 
             # Configure bst to pull but not push from a cache and run `bst push`.
             # This should also fail.
-            cli.configure({
+            cli.configure(add_client_config_creds({
                 'artifacts': {'url': share1.repo, 'push': False},
-            })
+            }, credentials))
             result = cli.run(project=project, args=['push', 'target.bst'])
             result.assert_main_error(ErrorDomain.STREAM, None)
 
             # Configure bst to push to one of the caches and run `bst push`. This works.
-            cli.configure({
+            cli.configure(add_client_config_creds({
                 'artifacts': [
                     {'url': share1.repo, 'push': False},
                     {'url': share2.repo, 'push': True},
                 ]
-            })
+            }, credentials))
             result = cli.run(project=project, args=['push', 'target.bst'])
 
             assert_not_shared(cli, share1, project, 'target.bst')
@@ -108,12 +153,12 @@ def test_push(cli, tmpdir, datafiles):
         # Now try pushing to both
 
         with create_artifact_share(os.path.join(str(tmpdir), 'artifactshare2')) as share2:
-            cli.configure({
+            cli.configure(add_client_config_creds({
                 'artifacts': [
                     {'url': share1.repo, 'push': True},
                     {'url': share2.repo, 'push': True},
                 ]
-            })
+            }, credentials))
             result = cli.run(project=project, args=['push', 'target.bst'])
 
             assert_shared(cli, share1, project, 'target.bst')
@@ -122,11 +167,16 @@ def test_push(cli, tmpdir, datafiles):
 
 # Tests that `bst push --deps all` pushes all dependencies of the given element.
 #
-@pytest.mark.datafiles(DATA_DIR)
-def test_push_all(cli, tmpdir, datafiles):
-    project = os.path.join(datafiles.dirname, datafiles.basename)
+@pytest.mark.parametrize(
+    'credential_filenames', CREDENTIAL_FILENAMES.values(), ids=list(CREDENTIAL_FILENAMES))
+@pytest.mark.datafiles(DATA_DIR, CRED_DIR, keep_top_dir=True)
+def test_push_all(cli, tmpdir, datafiles, credential_filenames):
+    project = os.path.join(datafiles, 'project')
+    credfiles = os.path.join(datafiles, 'creds')
 
-    with create_artifact_share(os.path.join(str(tmpdir), 'artifactshare')) as share:
+    credentials = join_credentials_path(credential_filenames, credfiles)
+
+    with create_artifact_share(os.path.join(str(tmpdir), 'artifactshare'), credentials=credentials) as share:
 
         # First build it without the artifact cache configured
         result = cli.run(project=project, args=['build', 'target.bst'])
@@ -136,7 +186,7 @@ def test_push_all(cli, tmpdir, datafiles):
         assert cli.get_element_state(project, 'target.bst') == 'cached'
 
         # Configure artifact share
-        cli.configure({
+        cli.configure(add_client_config_creds({
             #
             # FIXME: This test hangs "sometimes" if we allow
             #        concurrent push.
@@ -152,7 +202,7 @@ def test_push_all(cli, tmpdir, datafiles):
                 'url': share.repo,
                 'push': True,
             }
-        })
+        }, credentials))
 
         # Now try bst push all the deps
         result = cli.run(project=project, args=[
diff --git a/tests/testutils/artifactshare.py b/tests/testutils/artifactshare.py
index d7575e5..a486ca1 100644
--- a/tests/testutils/artifactshare.py
+++ b/tests/testutils/artifactshare.py
@@ -29,7 +29,7 @@ from buildstream._exceptions import ArtifactError
 #
 class ArtifactShare():
 
-    def __init__(self, directory, *, total_space=None, free_space=None):
+    def __init__(self, directory, *, total_space=None, free_space=None, credentials={}):
 
         # The working directory for the artifact share (in case it
         # needs to do something outside of it's backend's storage folder).
@@ -55,19 +55,24 @@ class ArtifactShare():
 
         q = Queue()
 
-        self.process = Process(target=self.run, args=(q,))
+        self.process = Process(target=self.run, args=(q, credentials))
         self.process.start()
 
         # Retrieve port from server subprocess
         port = q.get()
 
-        self.repo = 'http://localhost:{}'.format(port)
+        if credentials:
+            protocol = 'https'
+        else:
+            protocol = 'http'
+
+        self.repo = '{}://localhost:{}'.format(protocol, port)
 
     # run():
     #
     # Run the artifact server.
     #
-    def run(self, q):
+    def run(self, q, credentials):
         pytest_cov.embed.cleanup_on_sigterm()
 
         # Optionally mock statvfs
@@ -77,7 +82,7 @@ class ArtifactShare():
             os.statvfs = self._mock_statvfs
 
         server = create_server(self.repodir, enable_push=True)
-        port = setup_server(server, 'localhost', 0)
+        port = setup_server(server, 'localhost', 0, **credentials)
 
         server.start()
 
@@ -149,8 +154,8 @@ class ArtifactShare():
 # Create an ArtifactShare for use in a test case
 #
 @contextmanager
-def create_artifact_share(directory, *, total_space=None, free_space=None):
-    share = ArtifactShare(directory, total_space=total_space, free_space=free_space)
+def create_artifact_share(directory, *, total_space=None, free_space=None, credentials={}):
+    share = ArtifactShare(directory, total_space=total_space, free_space=free_space, credentials=credentials)
     try:
         yield share
     finally:


[buildstream] 01/02: Refactor casserver for better coverage

Posted by no...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

not-in-ldap pushed a commit to branch dp0/casserver-tests
in repository https://gitbox.apache.org/repos/asf/buildstream.git

commit 5b476ddd57382c06d8fdb190c7e3fa687e88b4eb
Author: Daniel Playle <dp...@bloomberg.net>
AuthorDate: Fri Aug 31 11:25:31 2018 +0100

    Refactor casserver for better coverage
    
    Previously we had the logic for setting up the ports on the server in
    server_main. This makes for messy testing of this functionality. As
    such, this setup logic has been moved to a public function which can be
    tested easily.
    
    This commit does not change any exists test, but modifies the testutil
    for creating a cache server.
---
 buildstream/_artifactcache/casserver.py | 72 ++++++++++++++++++++++++---------
 tests/testutils/artifactshare.py        |  4 +-
 2 files changed, 56 insertions(+), 20 deletions(-)

diff --git a/buildstream/_artifactcache/casserver.py b/buildstream/_artifactcache/casserver.py
index 0af6572..1baaec4 100644
--- a/buildstream/_artifactcache/casserver.py
+++ b/buildstream/_artifactcache/casserver.py
@@ -42,6 +42,11 @@ from .cascache import CASCache
 class ArtifactTooLargeException(Exception):
     pass
 
+class RequiresServerKeyPairException(Exception):
+    pass
+
+class RequiresServerKeyForClientAuthException(Exception):
+    pass
 
 # create_server():
 #
@@ -72,27 +77,37 @@ def create_server(repo, *, enable_push):
 
     return server
 
-
-@click.command(short_help="CAS Artifact Server")
-@click.option('--port', '-p', type=click.INT, required=True, help="Port number")
-@click.option('--server-key', help="Private server key for TLS (PEM-encoded)")
-@click.option('--server-cert', help="Public server certificate for TLS (PEM-encoded)")
-@click.option('--client-certs', help="Public client certificates for TLS (PEM-encoded)")
-@click.option('--enable-push', default=False, is_flag=True,
-              help="Allow clients to upload blobs and update artifact cache")
-@click.argument('repo')
-def server_main(repo, port, server_key, server_cert, client_certs, enable_push):
-    server = create_server(repo, enable_push=enable_push)
-
+# setup_server():
+#
+# Creates a port on the given server. This port either be secured or unsecured.
+# This is dependent on the optional credential arguments.
+#
+# Either none or both of server_key and server_cert must be specified. If
+# client_certs is specified, then both server_key and server_cert must be
+# specified.
+#
+# If the caller of this function does not care what port is used, this decision
+# can be made by the gRPC runtime by specifying port 0. The port that is
+# actually used is returned.
+#
+# Args:
+#     server (grpc.server): The server to open a port on
+#     address (str): The address to bind the port on
+#     port (int): The port number to bind on. 0 if the gRPC runtime should pick
+#     server_key (str): The filename of the server private key file
+#     server_cert (str): The filename of the server public cert file
+#     client_certs (str): The filename of the client public certs file
+#
+# Returns:
+#     int: The actual port that was opened for this server
+def setup_server(server, address, port, server_key=None, server_cert=None, client_certs=None):
     use_tls = bool(server_key)
 
     if bool(server_cert) != use_tls:
-        click.echo("ERROR: --server-key and --server-cert are both required for TLS", err=True)
-        sys.exit(-1)
+        raise RequiresServerKeyPairException()
 
     if client_certs and not use_tls:
-        click.echo("ERROR: --client-certs can only be used with --server-key", err=True)
-        sys.exit(-1)
+        raise RequiresServerKeyForClientAuthException()
 
     if use_tls:
         # Read public/private key pair
@@ -110,9 +125,30 @@ def server_main(repo, port, server_key, server_cert, client_certs, enable_push):
         credentials = grpc.ssl_server_credentials([(server_key_bytes, server_cert_bytes)],
                                                   root_certificates=client_certs_bytes,
                                                   require_client_auth=bool(client_certs))
-        server.add_secure_port('[::]:{}'.format(port), credentials)
+        return server.add_secure_port('{}:{}'.format(address, port), credentials)
     else:
-        server.add_insecure_port('[::]:{}'.format(port))
+        return server.add_insecure_port('{}:{}'.format(address, port))
+
+
+@click.command(short_help="CAS Artifact Server")
+@click.option('--port', '-p', type=click.INT, required=True, help="Port number")
+@click.option('--server-key', help="Private server key for TLS (PEM-encoded)")
+@click.option('--server-cert', help="Public server certificate for TLS (PEM-encoded)")
+@click.option('--client-certs', help="Public client certificates for TLS (PEM-encoded)")
+@click.option('--enable-push', default=False, is_flag=True,
+              help="Allow clients to upload blobs and update artifact cache")
+@click.argument('repo')
+def server_main(repo, port, server_key, server_cert, client_certs, enable_push):
+    server = create_server(repo, enable_push=enable_push)
+
+    try:
+        setup_server(server, '[::]', port, server_key, server_cert, client_certs)
+    except RequiresServerKeyPairException:
+        click.echo("ERROR: --server-key and --server-cert are both required for TLS", err=True)
+        sys.exit(-1)
+    except RequiresServerKeyForClientAuthException:
+        click.echo("ERROR: --client-certs can only be used with --server-key", err=True)
+        sys.exit(-1)
 
     # Run artifact server
     server.start()
diff --git a/tests/testutils/artifactshare.py b/tests/testutils/artifactshare.py
index 05e87a4..d7575e5 100644
--- a/tests/testutils/artifactshare.py
+++ b/tests/testutils/artifactshare.py
@@ -12,7 +12,7 @@ import pytest_cov
 
 from buildstream import _yaml
 from buildstream._artifactcache.cascache import CASCache
-from buildstream._artifactcache.casserver import create_server
+from buildstream._artifactcache.casserver import create_server, setup_server
 from buildstream._context import Context
 from buildstream._exceptions import ArtifactError
 
@@ -77,7 +77,7 @@ class ArtifactShare():
             os.statvfs = self._mock_statvfs
 
         server = create_server(self.repodir, enable_push=True)
-        port = server.add_insecure_port('localhost:0')
+        port = setup_server(server, 'localhost', 0)
 
         server.start()