"报道运行app.py"在docker容器里面没有创建.coverage文件?

时间:2018-02-08 18:24:57

标签: python docker containers dockerfile coverage.py

系统默认:

2012 macbook pro运行Sierra, Docker版本:17.12.0-ce-mac49(21995)

问题:

我在使用"覆盖运行"运行我的python应用程序时遇到问题在我的容器中。我将尝试解释我在下面做的事情。希望这是有道理的。试图尽可能多地了解我尝试做什么以及我已经在这里做了什么。

基本上我的情况就是这样。我正在开始基于python的(金字塔)Web服务。它只是一个基于HTTP的API。没有什么超级花哨的。我希望得到它的覆盖数,因为当我对它进行邮差测试时,所以我开始做#34;覆盖运行/path/path/path/app.py"这完全没问题。我创建了一个.coveragerc来归零我要分析的源代码,一切都很好,外面一个容器。试图让覆盖面工作INSIDE我的这个应用程序的容器化版本是另一回事。

供参考..这里是我的Dockerfile,其秘密内容被掩盖了#########标记

FROM ################:latest

# Add files
ADD . / /path/

# Set up Env
ENV GRAPHITE_SERVER=localhost
ENV APP_USER=root
ENV APP_HOME=/path
ENV APP_LOGS=$APP_HOME/logs
ENV PYTHONPATH=#################:$PYTHONPATH
ARG requested_environment=stage1
ARG coverage
ARG log_monitor
ENV COVERAGE=$coverage
ENV LOG_MONITOR=$log_monitor
ENV ENVIRONMENT=$requested_environment
ENV PATH=$APP_HOME/######:$PATH

RUN mkdir -p $APP_LOGS
RUN pip install -r $APP_HOME/requirements.txt
RUN cd $APP_HOME/external-libs/mysql-connector/mysql-connector-python-2.1.2/; python setup.py install

RUN ln -fs /usr/bin/python /usr/local/bin/python

EXPOSE 1234

# start app
ENTRYPOINT $COVERAGE $APP_HOME/path/path/app.py $LOG_MONITOR

我的构建命令是

docker build . --build-arg requested_environment=stage1 --build-arg log_monitor=noMonitor --build-arg coverage="coverage run" -t stage1-latest

我想在容器内做同样的事情,因为我可以在容器外做..意思是..我想在容器内运行应用程序覆盖。所以你认为它就像正常构建容器一样简单,入口点通常是

ENTRYPOINT /path/path/path/app.py
changed to
ENTRYPOINT coverage run /path/path/path/app.py

但显然不是。

如果我使用ENTRYPOINT或CMD构建容器启动应用程序..就像这些......是的,我已经尝试过所有这些......

ENTRYPOINT coverage run /path/path/path/app.py
ENTRYPOINT bash -c "coverage run /path/path/path/app.py"
ENTRYPOINT ["bash' "-c","coverage run /path/path/path/app.py"]
ENTRYPOINT ["coverage","run","/path/path/path/app.py"]
CMD coverage run /path/path/path/app.py
CMD ["bash' "-c","coverage run /path/path/path/app.py"]
or
CMD ["coverage","run","/path/path/path/app.py"]

然后像这样启动容器

docker run --name stage1-latest-container-instance -itp 1234:1234 stage1-latest

甚至在dockerfile中的CMD条目的情况下(因为它们是默认的,我用这个命令覆盖它们)

docker run --name stage1-latest-container-instance -itp 1234:1234 stage1-latest coverage run /path/path/path/app.py

我的应用程序启动完全正常..没有问题..但是

没有创建.coverage文件!没有在容器的文件系统中我看到.coverage文件..

好的..尝试差异方法

将dockerfile更改为

ENTRYPOINT bash
or
CMD bash

重建它......

然后运行容器 docker run --name stage1-latest-container-instance -itp 1234:1234 stage1-latest

在docker容器中给出了提示。

[root@ac25fb69bb2e /]#

然后从另一个窗口执行此操作

docker exec -it stage1-latest-container-instance coverage run /path/path/path/app.py

或其他方法来执行这个docker exec,如

docker exec -it stage1-latest-container-instance bash -c "coverage run /path/path/path/app.py"

STILL!没有.coverage文件被创建。

好的......走向差异化的方法 我甚至创建了一个名为runapp.sh的文件,并在其中添加以下行

#!/bin/bash
cd /path/path/path && coverage run app.py 

然后用

启动容器

docker run --name stage1-latest-container-instance -itp 1234:1234 stage1-latest /path/runapp.sh

没有.coverage文件 然后我试着用

运行容器

docker run --name stage1-latest-container-instance -itp 1234:1234 stage1-latest bash

然后从另一个窗口执行

docker exec -it stage1-latest-container-instance /path/runapp.sh

仍然没有.container文件

好的..最后的手段..

进入我运行" docker run"之后的提示。只需" CMD bash"在交互模式的dockerfile中(-it)..然后手动启动我的app覆盖范围。

[root@ac25fb69bb2e /]# coverage run /path/path/path/app.py

并且令人惊讶..创建.coverage文件没问题....

检查什么! 我无法想出这个......我不知道发生了什么......但是从容器外部对容器执行的命令与在内部手动执行的命令之间似乎存在完全脱节容器内部的单个shell是否可以创建文件。

有人请告诉我我在这里做错了什么。

我能看到的唯一下一步是使用我的应用程序内部的coverage模块,并为KeyboardInterrupt添加一个异常处理程序,以便在应用程序被终止时保存覆盖率数据。但那很糟糕。

4 个答案:

答案 0 :(得分:1)

这并不是我原本想要的,但这是我最终必须做的才能让这一切正常运作。将此作为我自己问题的答案,以防其他人发现自己陷入同样的​​困境,并想知道我是如何解决这个问题的。

我最终不得不在app.py脚本中使用coverage api。我无法理解为什么使用"覆盖运行"在运行容器时作为入口点或cmd传入时不起作用。仍然不确定为什么。如果有人愿意分享,如果他们知道原因,就会喜欢它。

在我的app.py中,我做了以下

当它最初启动时,我检查是否正在设置我正在调用的运行时标志" withcoverage"。

if withcoverage:
    print "starting code coverage engine"
    import coverage
    os.system("mkdir %s/tmpcoveragedata" % os.environ['HEYTHERE_HOME'])
    coveragedatafile = ".coverage-"+str(int(time.time()))
    cov = coverage.Coverage(data_file=os.environ['APP_HOME']+"/tmpcoveragedata/"+coveragedatafile,config_file=os.environ['APP_HOME']+"/.coveragerc")
    cov.start()

然后我继续正常启动服务器。 但是我必须添加一个try:block来捕获KeyboardInterrupt(SIGINT)并在退出时保存覆盖范围。像这样:

try:
    # start the server etc.. etc..
    .
    .
    .
except KeyboardInterrupt as e:
    if withcoverage:
        print "saving coverage stats"
        save_coverage(cov)
        os.system("bash -c 'cd %s/tmpcoveragedata && rm -f .coverage ; cd %s/tmpcoveragedata && ln -s %s .coverage" % (os.environ['APP_HOME'],os.environ['APP_HOME'],coveragedatafile))
        os.system("bash -c 'cd %s/tmpcoveragedata/ && coverage report > %s/tmpcoveragedata/coverage-report-%s.txt'" % (os.environ['APP_HOME'],os.environ['APP_HOME'],coveragedatafile))
    print "exiting program"
    exit()

当我构建我的容器时,我在主源代码库中将一个卷挂载到容器内的tmpcoveragedata目录。然后,容器收到SIGINT并退出时生成的coverage文件和报告将保存到该临时文件夹中,该文件夹将映射到运行容器的计算机上的相应coverage数据存储。

这是我的码头运行命令

docker run --name stage1-latest-container-instance -d -v /app_home/coveragedata:/app_home/tmpcoveragedata -p 1234:1234 stage1-latest

这现在正在运行..它将以覆盖范围运行应用程序..使用SIGINT保存退出时的覆盖范围。

但有一点需要注意。当停止容器时一个简单的

docker stop <container-name> 

是不够的。我需要执行以下操作,以便应用程序在我的自动化停止容器时看到KeyboardInterrupt。

docker kill --signal=SIGINT stage1-latest-container-instance

希望这可以帮助其他任何人解决这个问题。

答案 1 :(得分:1)

对于像我一样在Google绊脚石的任何人...

为扩展Wolfium的答案,这是由于docker在覆盖率已将数据写入其数据文件之前杀死了python进程(或更正确地说,python无法优雅地解释docker的默认SIGTERM信号,并且陷入了几秒钟后它得到SIGKILL时就会堆积)。解决此问题的最快方法是:

  • 确保python进程能够接收相关信号。如果您是在shell入口点脚本中运行此脚本,则可能需要以exec开头,例如exec coverage -m swagger_server
  • 默认将图像的停止信号设置为SIGINT,这是大多数python处理程序实现的:将行STOPSIGNAL SIGINT添加到Dockerfile中。

更好的解决方案是确保您的python处理程序可以正确响应SIGTERM

答案 2 :(得分:0)

很好的信息和详细信息,解释和显示变通方法。 它帮助了我很多正确的方向。

根本原因是在docker中运行时,修补的覆盖服务正在运行,并且在docker中作为入口点运行的覆盖范围在完成运行之前不会保存。

所以,如果你能像你那样打破它,那对我来说也是如此。 我正在使用Flask,我发现它使用的开发服务器捕获密钥中断异常,我无法将覆盖范围内的服务服务器关闭。 最后,我找到了一种方法来获取一条路径,从内部向烧瓶发送器服务器发出信号,它就像你在第一时间向我展示的魅力一样。

非常感谢分享...

答案 3 :(得分:0)

在@ Andy-T的答案的基础上,我发现Python本身(或者可能只有Flask dev服务器)没有适当地关闭以响应SIGTERM信号,即使在docker外部也是如此。因此,通过为SIGTERM注册信号处理程序,我能够调用sys.exit(),这为coverage.py提供了写出数据的机会。

这是我的app.py文件:

import sys
import signal
from flask import Flask


def exit_cleanly(signal_number, stackframe):
    sys.exit()


signal.signal(signal.SIGTERM, exit_cleanly)

app = Flask(__name__)


@app.route("/")
def hello():
    return "Hello World!"

关键点在这里:

  • 标准库中的signal模块允许您在python从操作系统接收到特定信号时将函数注册为“回调”。
  • signal.SIGTERMkill命令在linux和macos上发送的默认信号
  • sys.exit(不带参数)允许python解释器关闭,完成所有正常的清理步骤-包括注册为在出口运行的coverage.py代码

然后,在我的(macos或linux)终端内,我运行以下命令以提供覆盖数据,而没有docker

python3 -m venv venv
. venv/bin/activate
pip3 install Flask
coverage run -m flask run &> /tmp/flask.log &
curl http://localhost:5000/
kill "$(ps -ef | grep 'coverage run' | grep -v grep | awk '{ print $2 }')"
coverage report

详细信息:

  • 前两行是完全可选的,但建议将依赖关系隔离到虚拟环境中(与OP不相关)
  • coverage run -m flask run &> /tmp/flask.log &将stderr / stdout放入/ tmp目录中的文件中,以使终端不会混乱和不可读(&> /tmp/flask.log)。它还会将进程发送到后台(尾随&
  • curl http://localhost:5000/ -仅作为Flask应用程序的轻量级测试而发出请求。您可以将其替换为实际的测试运行(也称为py.test或行为或其他)
  • kill "$(ps -ef | grep 'coverage run' | grep -v grep | awk '{ print $2 }')"-搜索带有ps -ef的正在运行的进程,然后过滤到仅包含coverage run的进程,然后将其自身删除(grep -v表示删除匹配的行),然后查找带有PID的第二个字段。最后,kill将SIGTERM发送到PID。因此,这应该导致exit_cleanly()回调触发并拆除flask进程。
  • coverage report只是将摘要打印到终端,以便您查看其是否有效

要在docker中运行它,我将所有这些包装到一个bash脚本中,并使其成为docker容器的入口点。这是一个存储库,它对所有这些工作进行了哈希处理,并与CircleCI + Codecov集成为示例示例: https://github.com/brycefisher/coveragepy-flask