New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
wip - add WSL and SSH remotes #16109
Closed
jcar87
wants to merge
23
commits into
conan-io:feature/docker_wrapper
from
jcar87:lcc/feature/ssh-remote
Closed
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
983bf19
conan create remote WIP
davidsanfal 8089481
runners added
davidsanfal d27e767
docker runner updated
davidsanfal 47ff6be
docker runner updated
davidsanfal b3e44c3
abs path added
davidsanfal e03cdd9
linux slashes fixed
davidsanfal e151eec
info updated
davidsanfal bffc1d5
restore only new packages
davidsanfal ee6aaa6
some info added
davidsanfal 9b7da81
basic test added
davidsanfal 2229767
new test added
davidsanfal ad830ed
requiremnets fixed
davidsanfal 32c089e
new docker exec added
davidsanfal 00404ac
docker added to dev requirements
davidsanfal 35b6cec
docker conan version check added
davidsanfal 1071ae4
more info added
davidsanfal 4a0a474
dockerfile interface updated
davidsanfal 47be1f6
docker added as a extras_require
davidsanfal c0e2979
some fix
davidsanfal 0c2117d
ssh remote WIP
jcar87 09d41b7
Run remote create command
jcar87 d9ea248
Restore remote cache
jcar87 ab4895d
Add super rough implementation of wsl runner
jcar87 File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
class RunnerException(Exception): | ||
def __init__(self, *args, **kwargs): | ||
self.command = kwargs.pop("command", None) | ||
self.stdout_log = kwargs.pop("stdout_log", None) | ||
self.stderr_log = kwargs.pop("stderr_log", None) | ||
super(RunnerException, self).__init__(*args, **kwargs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
import os | ||
import json | ||
import platform | ||
import shutil | ||
from conan.api.model import ListPattern | ||
from conan.api.output import Color, ConanOutput | ||
from conan.api.conan_api import ConfigAPI | ||
from conan.cli import make_abs_path | ||
from conan.internal.runner import RunnerException | ||
from conans.client.profile_loader import ProfileLoader | ||
from conans.errors import ConanException | ||
from conans.model.version import Version | ||
|
||
|
||
def docker_info(msg, error=False): | ||
fg=Color.BRIGHT_MAGENTA | ||
if error: | ||
fg=Color.BRIGHT_RED | ||
ConanOutput().status('\n┌'+'─'*(2+len(msg))+'┐', fg=fg) | ||
ConanOutput().status(f'| {msg} |', fg=fg) | ||
ConanOutput().status('└'+'─'*(2+len(msg))+'┘\n', fg=fg) | ||
|
||
|
||
def list_patterns(cache_info): | ||
_pattern = [] | ||
for reference, info in cache_info.items(): | ||
for revisions in info.get('revisions', {}).values(): | ||
for package in revisions.get('packages').keys(): | ||
_pattern.append(f'{reference}:{package}') | ||
return _pattern | ||
|
||
|
||
class DockerRunner: | ||
def __init__(self, conan_api, command, profile, args, raw_args): | ||
import docker | ||
import docker.api.build | ||
try: | ||
self.docker_client = docker.from_env() | ||
self.docker_api = docker.APIClient() | ||
docker.api.build.process_dockerfile = lambda dockerfile, path: ('Dockerfile', dockerfile) | ||
except: | ||
raise ConanException("Docker Client failed to initialize." | ||
"\n - Check if docker is installed and running" | ||
"\n - Run 'pip install docker>=5.0.0, <=5.0.3'") | ||
self.conan_api = conan_api | ||
self.args = args | ||
self.abs_host_path = make_abs_path(args.path) | ||
if args.format: | ||
raise ConanException("format argument is forbidden if running in a docker runner") | ||
self.abs_runner_home_path = os.path.join(self.abs_host_path, '.conanrunner') | ||
self.abs_docker_path = os.path.join('/root/conanrunner', os.path.basename(self.abs_host_path)).replace("\\","/") | ||
raw_args[raw_args.index(args.path)] = f'"{self.abs_docker_path}"' | ||
self.command = ' '.join([f'conan {command}'] + raw_args + ['-f json > create.json']) | ||
self.dockerfile = profile.runner.get('dockerfile') | ||
self.docker_build_context = profile.runner.get('docker_build_context') | ||
self.image = profile.runner.get('image') | ||
if not (self.dockerfile or self.image): | ||
raise ConanException("'dockerfile' or docker image name is needed") | ||
self.image = self.image or 'conan-runner-default' | ||
self.name = f'conan-runner-{profile.runner.get("suffix", "docker")}' | ||
self.remove = str(profile.runner.get('remove', 'false')).lower() == 'true' | ||
self.cache = str(profile.runner.get('cache', 'clean')) | ||
self.container = None | ||
|
||
def run(self): | ||
""" | ||
run conan inside a Docker continer | ||
""" | ||
if self.dockerfile: | ||
docker_info(f'Building the Docker image: {self.image}') | ||
self.build_image() | ||
volumes, environment = self.create_runner_environment() | ||
error = False | ||
try: | ||
if self.docker_client.containers.list(all=True, filters={'name': self.name}): | ||
docker_info('Starting the docker container') | ||
self.container = self.docker_client.containers.get(self.name) | ||
self.container.start() | ||
else: | ||
docker_info('Creating the docker container') | ||
self.container = self.docker_client.containers.run( | ||
self.image, | ||
"/bin/bash -c 'while true; do sleep 30; done;'", | ||
name=self.name, | ||
volumes=volumes, | ||
environment=environment, | ||
detach=True, | ||
auto_remove=False) | ||
docker_info(f'Container {self.name} running') | ||
except Exception as e: | ||
raise ConanException(f'Imposible to run the container "{self.name}" with image "{self.image}"' | ||
f'\n\n{str(e)}') | ||
try: | ||
self.init_container() | ||
self.run_command(self.command) | ||
self.update_local_cache() | ||
except ConanException as e: | ||
error = True | ||
raise e | ||
except RunnerException as e: | ||
error = True | ||
raise ConanException(f'"{e.command}" inside docker fail' | ||
f'\n\nLast command output: {str(e.stdout_log)}') | ||
finally: | ||
if self.container: | ||
error_prefix = 'ERROR: ' if error else '' | ||
docker_info(f'{error_prefix}Stopping container', error) | ||
self.container.stop() | ||
if self.remove: | ||
docker_info(f'{error_prefix}Removing container', error) | ||
self.container.remove() | ||
|
||
def build_image(self): | ||
dockerfile_file_path = self.dockerfile | ||
if os.path.isdir(self.dockerfile): | ||
dockerfile_file_path = os.path.join(self.dockerfile, 'Dockerfile') | ||
with open(dockerfile_file_path) as f: | ||
build_path = self.docker_build_context or os.path.dirname(dockerfile_file_path) | ||
ConanOutput().highlight(f"Dockerfile path: '{dockerfile_file_path}'") | ||
ConanOutput().highlight(f"Docker build context: '{build_path}'\n") | ||
docker_build_logs = self.docker_api.build(path=build_path, dockerfile=f.read(), tag=self.image) | ||
for chunk in docker_build_logs: | ||
for line in chunk.decode("utf-8").split('\r\n'): | ||
if line: | ||
stream = json.loads(line).get('stream') | ||
if stream: | ||
ConanOutput().status(stream.strip()) | ||
|
||
def run_command(self, command, log=True): | ||
if log: | ||
docker_info(f'Running in container: "{command}"') | ||
exec_instance = self.docker_api.exec_create(self.container.id, f"/bin/bash -c '{command}'", tty=True) | ||
exec_output = self.docker_api.exec_start(exec_instance['Id'], tty=True, stream=True, demux=True,) | ||
stderr_log, stdout_log = '', '' | ||
try: | ||
for (stdout_out, stderr_out) in exec_output: | ||
if stdout_out is not None: | ||
stdout_log += stdout_out.decode('utf-8', errors='ignore').strip() | ||
if log: | ||
ConanOutput().status(stdout_out.decode('utf-8', errors='ignore').strip()) | ||
if stderr_out is not None: | ||
stderr_log += stderr_out.decode('utf-8', errors='ignore').strip() | ||
if log: | ||
ConanOutput().status(stderr_out.decode('utf-8', errors='ignore').strip()) | ||
except Exception as e: | ||
if platform.system() == 'Windows': | ||
import pywintypes | ||
if isinstance(e, pywintypes.error): | ||
pass | ||
else: | ||
raise e | ||
exit_metadata = self.docker_api.exec_inspect(exec_instance['Id']) | ||
if exit_metadata['Running'] or exit_metadata['ExitCode'] > 0: | ||
raise RunnerException(command=command, stdout_log=stdout_log, stderr_log=stderr_log) | ||
return stdout_log, stderr_log | ||
|
||
def create_runner_environment(self): | ||
volumes = {self.abs_host_path: {'bind': self.abs_docker_path, 'mode': 'rw'}} | ||
environment = {'CONAN_RUNNER_ENVIRONMENT': '1'} | ||
if self.cache == 'shared': | ||
volumes[ConfigAPI(self.conan_api).home()] = {'bind': '/root/.conan2', 'mode': 'rw'} | ||
if self.cache in ['clean', 'copy']: | ||
shutil.rmtree(self.abs_runner_home_path, ignore_errors=True) | ||
os.mkdir(self.abs_runner_home_path) | ||
os.mkdir(os.path.join(self.abs_runner_home_path, 'profiles')) | ||
for file_name in ['global.conf', 'settings.yml', 'remotes.json']: | ||
src_file = os.path.join(ConfigAPI(self.conan_api).home(), file_name) | ||
if os.path.exists(src_file): | ||
shutil.copy(src_file, os.path.join(self.abs_runner_home_path, file_name)) | ||
self._copy_profiles(self.args.profile_build) | ||
self._copy_profiles(self.args.profile_host) | ||
if self.cache == 'copy': | ||
tgz_path = os.path.join(self.abs_runner_home_path, 'local_cache_save.tgz') | ||
docker_info(f'Save host cache in: {tgz_path}') | ||
self.conan_api.cache.save(self.conan_api.list.select(ListPattern("*:*")), tgz_path) | ||
return volumes, environment | ||
|
||
def init_container(self): | ||
min_conan_version = '2.1' | ||
stdout, _ = self.run_command('conan --version', log=True) | ||
docker_conan_version = str(stdout.replace('Conan version ', '').replace('\n', '').replace('\r', '')) # Remove all characters and color | ||
if Version(docker_conan_version) <= Version(min_conan_version): | ||
ConanOutput().status(f'ERROR: conan version inside the container must be greater than {min_conan_version}', fg=Color.BRIGHT_RED) | ||
raise ConanException( f'conan version inside the container must be greater than {min_conan_version}') | ||
if self.cache == 'shared': | ||
self.run_command('mkdir -p ${HOME}/.conan2/profiles', log=False) | ||
self.run_command('cp -r "'+self.abs_docker_path+'/.conanrunner/profiles/." ${HOME}/.conan2/profiles/.', log=False) | ||
for file_name in ['global.conf', 'settings.yml', 'remotes.json']: | ||
if os.path.exists( os.path.join(self.abs_runner_home_path, file_name)): | ||
self.run_command('cp "'+self.abs_docker_path+'/.conanrunner/'+file_name+'" ${HOME}/.conan2/'+file_name, log=False) | ||
if self.cache in ['copy']: | ||
self.run_command('conan cache restore "'+self.abs_docker_path+'/.conanrunner/local_cache_save.tgz"') | ||
|
||
def update_local_cache(self): | ||
if self.cache != 'shared': | ||
self.run_command('conan list --graph=create.json --graph-binaries=build --format=json > pkglist.json', log=False) | ||
self.run_command('conan cache save --list=pkglist.json --file "'+self.abs_docker_path+'"/.conanrunner/docker_cache_save.tgz') | ||
tgz_path = os.path.join(self.abs_runner_home_path, 'docker_cache_save.tgz') | ||
docker_info(f'Restore host cache from: {tgz_path}') | ||
package_list = self.conan_api.cache.restore(tgz_path) | ||
|
||
def _copy_profiles(self, profiles): | ||
cwd = os.getcwd() | ||
if profiles: | ||
for profile in profiles: | ||
profile_path = ProfileLoader.get_profile_path(os.path.join(ConfigAPI(self.conan_api).home(), 'profiles'), profile, cwd) | ||
shutil.copy(profile_path, os.path.join(self.abs_runner_home_path, 'profiles', os.path.basename(profile_path))) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit more UX-friendly ready, as users will miss or mistype the
type
.