mirror of
				https://github.com/f-droid/fdroidserver.git
				synced 2025-11-04 06:30:27 +03:00 
			
		
		
		
	new subcommand "exec": podman implementation
This commit is contained in:
		
							parent
							
								
									fd60436aa3
								
							
						
					
					
						commit
						63660e1aed
					
				
					 4 changed files with 176 additions and 0 deletions
				
			
		| 
						 | 
				
			
			@ -66,6 +66,7 @@ COMMANDS = OrderedDict([
 | 
			
		|||
# modify the local environment, or even run things as root.
 | 
			
		||||
COMMANDS_INTERNAL = [
 | 
			
		||||
    "destroy",
 | 
			
		||||
    "exec",
 | 
			
		||||
    "up",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,6 +67,7 @@ import logging
 | 
			
		|||
import operator
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import shlex
 | 
			
		||||
import shutil
 | 
			
		||||
import socket
 | 
			
		||||
import stat
 | 
			
		||||
| 
						 | 
				
			
			@ -110,6 +111,11 @@ FDROID_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))
 | 
			
		|||
 | 
			
		||||
SUPPORTED_VIRT_CONTAINER_TYPES = ('podman', 'vagrant')
 | 
			
		||||
 | 
			
		||||
# The path to the homedir where apps are built in the container/VM.
 | 
			
		||||
BUILD_HOME = '/home/vagrant'
 | 
			
		||||
# Username inside of build containers/VMs used for runing builds.
 | 
			
		||||
BUILD_USER = 'vagrant'
 | 
			
		||||
 | 
			
		||||
# There needs to be a default, and this is the most common for software.
 | 
			
		||||
DEFAULT_LOCALE = 'en-US'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -5129,6 +5135,42 @@ def get_podman_container(appid, vercode):
 | 
			
		|||
    return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def inside_exec(appid, vercode, command, virt_container_type):
 | 
			
		||||
    """Execute the command inside of the VM for the build."""
 | 
			
		||||
    if virt_container_type == 'vagrant':
 | 
			
		||||
        return vagrant_exec(appid, vercode, command)
 | 
			
		||||
    elif virt_container_type == 'podman':
 | 
			
		||||
        return podman_exec(appid, vercode, command)
 | 
			
		||||
    else:
 | 
			
		||||
        raise Exception(
 | 
			
		||||
            f"'{virt_container_type}' not supported, currently supported: vagrant, podman"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def podman_exec(appid, vercode, command):
 | 
			
		||||
    """Execute the command inside of a podman container for the build."""
 | 
			
		||||
    container_name = get_container_name(appid, vercode)
 | 
			
		||||
    to_stdin = shlex.join(command)
 | 
			
		||||
    p = subprocess.run(
 | 
			
		||||
        [
 | 
			
		||||
            'podman',
 | 
			
		||||
            'exec',
 | 
			
		||||
            '--interactive',
 | 
			
		||||
            '--user=vagrant',
 | 
			
		||||
            f'--workdir={BUILD_HOME}',
 | 
			
		||||
            container_name,
 | 
			
		||||
        ]
 | 
			
		||||
        + ['/bin/bash', '-e', '-l', '-x'],  # the shell that runs the command
 | 
			
		||||
        input=to_stdin,
 | 
			
		||||
        text=True,
 | 
			
		||||
        stderr=subprocess.STDOUT,
 | 
			
		||||
    )
 | 
			
		||||
    if p.returncode != 0:
 | 
			
		||||
        raise subprocess.CalledProcessError(
 | 
			
		||||
            p.returncode, f"{to_stdin} | {' '.join(p.args)}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_vagrantfile_path(appid, vercode):
 | 
			
		||||
    """Return the path for the unique VM for a given build.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -5139,6 +5181,10 @@ def get_vagrantfile_path(appid, vercode):
 | 
			
		|||
    return Path('tmp/buildserver', get_container_name(appid, vercode), 'Vagrantfile')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def vagrant_exec(appid, vercode, command):
 | 
			
		||||
    """Execute a command in the Vagrant VM via ssh."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def vagrant_destroy(appid, vercode):
 | 
			
		||||
    import vagrant
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										66
									
								
								fdroidserver/exec.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								fdroidserver/exec.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,66 @@
 | 
			
		|||
#!/usr/bin/env python3
 | 
			
		||||
#
 | 
			
		||||
# exec.py - part of the FDroid server tools
 | 
			
		||||
# Copyright (C) 2024-2025, Hans-Christoph Steiner <hans@eds.org>
 | 
			
		||||
#
 | 
			
		||||
# This program is free software: you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
# the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU Affero General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
"""Run an fdroidserver subcommand inside of the VM for the build.
 | 
			
		||||
 | 
			
		||||
Since this is an internal command, the strings are not localized.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
import logging
 | 
			
		||||
import traceback
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
 | 
			
		||||
from . import common
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    parser = ArgumentParser(
 | 
			
		||||
        description="Run a subcommand in the buildserver container/box."
 | 
			
		||||
    )
 | 
			
		||||
    common.setup_global_opts(parser)
 | 
			
		||||
    common.setup_virt_container_type_opts(parser)
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "APPID:VERCODE",
 | 
			
		||||
        help="Application ID with Version Code in the form APPID:VERCODE",
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "COMMAND", nargs="*", help="Command to run inside the container/box."
 | 
			
		||||
    )
 | 
			
		||||
    options = common.parse_args(parser)
 | 
			
		||||
    common.set_console_logging(options.verbose)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        appid, vercode = common.split_pkg_arg(options.__dict__['APPID:VERCODE'])
 | 
			
		||||
        common.inside_exec(
 | 
			
		||||
            appid,
 | 
			
		||||
            vercode,
 | 
			
		||||
            options.COMMAND,
 | 
			
		||||
            common.get_virt_container_type(options),
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        if options.verbose:
 | 
			
		||||
            logging.error(traceback.format_exc())
 | 
			
		||||
        else:
 | 
			
		||||
            logging.error(e)
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										63
									
								
								tests/test_exec.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										63
									
								
								tests/test_exec.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
#!/usr/bin/env python3
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from unittest import mock, skipUnless
 | 
			
		||||
 | 
			
		||||
from fdroidserver import common, exception, exec
 | 
			
		||||
from .shared_test_code import mkdtemp, APPID, VERCODE, APPID_VERCODE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExecTest(unittest.TestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self._td = mkdtemp()
 | 
			
		||||
        self.testdir = self._td.name
 | 
			
		||||
        os.chdir(self.testdir)
 | 
			
		||||
        common.config = dict()
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        self._td.cleanup()
 | 
			
		||||
        common.config = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Exec_main(ExecTest):
 | 
			
		||||
    @mock.patch('sys.argv', ['fdroid exec', APPID_VERCODE])
 | 
			
		||||
    @mock.patch('fdroidserver.common.get_default_cachedir')
 | 
			
		||||
    @mock.patch('fdroidserver.common.podman_exec')
 | 
			
		||||
    def test_podman(self, podman_exec, get_default_cachedir):
 | 
			
		||||
        get_default_cachedir.return_value = self.testdir
 | 
			
		||||
        common.config['virt_container_type'] = 'podman'
 | 
			
		||||
        exec.main()
 | 
			
		||||
        podman_exec.assert_called_once()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@skipUnless(os.path.isdir('/run/podman'), 'Requires Podman to run.')
 | 
			
		||||
class Exec_podman_exec(ExecTest):
 | 
			
		||||
    def _only_run_if_container_exists(self):
 | 
			
		||||
        try:
 | 
			
		||||
            common.get_podman_container(APPID, VERCODE)
 | 
			
		||||
        except exception.BuildException as e:
 | 
			
		||||
            # To run these tests, first do: `./fdroid up com.example:123`
 | 
			
		||||
            self.skipTest(f'Requires Podman container {APPID_VERCODE} to run: {e}')
 | 
			
		||||
 | 
			
		||||
    def test_no_existing_container(self):
 | 
			
		||||
        appid = 'should.never.exist'
 | 
			
		||||
        f = Path(f'metadata/{appid}.yml')
 | 
			
		||||
        f.parent.mkdir()
 | 
			
		||||
        f.write_text(f.name)
 | 
			
		||||
        with self.assertRaises(subprocess.CalledProcessError) as e:
 | 
			
		||||
            common.podman_exec(appid, 9999999999, ['ls'])
 | 
			
		||||
            self.assertEqual(e.exception.returncode, 1)
 | 
			
		||||
 | 
			
		||||
    def test_clean_run(self):
 | 
			
		||||
        self._only_run_if_container_exists()
 | 
			
		||||
        common.podman_exec(APPID, VERCODE, ['ls'])
 | 
			
		||||
 | 
			
		||||
    def test_error_run(self):
 | 
			
		||||
        self._only_run_if_container_exists()
 | 
			
		||||
        with self.assertRaises(subprocess.CalledProcessError) as e:
 | 
			
		||||
            common.podman_exec(APPID, VERCODE, ['/bin/false'])
 | 
			
		||||
            self.assertEqual(e.exception.returncode, 1)
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue