mirror of
https://github.com/docker/compose.git
synced 2025-07-23 21:54:40 +02:00
commit
5be6bde76c
@ -17,10 +17,16 @@ class DocoptDispatcher:
|
|||||||
self.command_class = command_class
|
self.command_class = command_class
|
||||||
self.options = options
|
self.options = options
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_command_and_options(cls, doc_entity, argv, options):
|
||||||
|
command_help = getdoc(doc_entity)
|
||||||
|
opt = docopt_full_help(command_help, argv, **options)
|
||||||
|
command = opt['COMMAND']
|
||||||
|
return command_help, opt, command
|
||||||
|
|
||||||
def parse(self, argv):
|
def parse(self, argv):
|
||||||
command_help = getdoc(self.command_class)
|
command_help, options, command = DocoptDispatcher.get_command_and_options(
|
||||||
options = docopt_full_help(command_help, argv, **self.options)
|
self.command_class, argv, self.options)
|
||||||
command = options['COMMAND']
|
|
||||||
|
|
||||||
if command is None:
|
if command is None:
|
||||||
raise SystemExit(command_help)
|
raise SystemExit(command_help)
|
||||||
|
@ -26,6 +26,7 @@ from ..config.serialize import serialize_config
|
|||||||
from ..config.types import VolumeSpec
|
from ..config.types import VolumeSpec
|
||||||
from ..const import IS_WINDOWS_PLATFORM
|
from ..const import IS_WINDOWS_PLATFORM
|
||||||
from ..errors import StreamParseError
|
from ..errors import StreamParseError
|
||||||
|
from ..metrics.decorator import metrics
|
||||||
from ..progress_stream import StreamOutputError
|
from ..progress_stream import StreamOutputError
|
||||||
from ..project import get_image_digests
|
from ..project import get_image_digests
|
||||||
from ..project import MissingDigests
|
from ..project import MissingDigests
|
||||||
@ -53,6 +54,8 @@ from .log_printer import LogPrinter
|
|||||||
from .utils import get_version_info
|
from .utils import get_version_info
|
||||||
from .utils import human_readable_file_size
|
from .utils import human_readable_file_size
|
||||||
from .utils import yesno
|
from .utils import yesno
|
||||||
|
from compose.metrics.client import MetricsCommand
|
||||||
|
from compose.metrics.client import Status
|
||||||
|
|
||||||
|
|
||||||
if not IS_WINDOWS_PLATFORM:
|
if not IS_WINDOWS_PLATFORM:
|
||||||
@ -62,36 +65,77 @@ log = logging.getLogger(__name__)
|
|||||||
console_handler = logging.StreamHandler(sys.stderr)
|
console_handler = logging.StreamHandler(sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(): # noqa: C901
|
||||||
signals.ignore_sigpipe()
|
signals.ignore_sigpipe()
|
||||||
|
command = None
|
||||||
try:
|
try:
|
||||||
command = dispatch()
|
_, opts, command = DocoptDispatcher.get_command_and_options(
|
||||||
command()
|
TopLevelCommand,
|
||||||
|
get_filtered_args(sys.argv[1:]),
|
||||||
|
{'options_first': True, 'version': get_version_info('compose')})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
command_func = dispatch()
|
||||||
|
command_func()
|
||||||
except (KeyboardInterrupt, signals.ShutdownException):
|
except (KeyboardInterrupt, signals.ShutdownException):
|
||||||
log.error("Aborting.")
|
exit_with_metrics(command, "Aborting.", status=Status.FAILURE)
|
||||||
sys.exit(1)
|
|
||||||
except (UserError, NoSuchService, ConfigurationError,
|
except (UserError, NoSuchService, ConfigurationError,
|
||||||
ProjectError, OperationFailedError) as e:
|
ProjectError, OperationFailedError) as e:
|
||||||
log.error(e.msg)
|
exit_with_metrics(command, e.msg, status=Status.FAILURE)
|
||||||
sys.exit(1)
|
|
||||||
except BuildError as e:
|
except BuildError as e:
|
||||||
reason = ""
|
reason = ""
|
||||||
if e.reason:
|
if e.reason:
|
||||||
reason = " : " + e.reason
|
reason = " : " + e.reason
|
||||||
log.error("Service '{}' failed to build{}".format(e.service.name, reason))
|
exit_with_metrics(command,
|
||||||
sys.exit(1)
|
"Service '{}' failed to build{}".format(e.service.name, reason),
|
||||||
|
status=Status.FAILURE)
|
||||||
except StreamOutputError as e:
|
except StreamOutputError as e:
|
||||||
log.error(e)
|
exit_with_metrics(command, e, status=Status.FAILURE)
|
||||||
sys.exit(1)
|
|
||||||
except NeedsBuildError as e:
|
except NeedsBuildError as e:
|
||||||
log.error("Service '{}' needs to be built, but --no-build was passed.".format(e.service.name))
|
exit_with_metrics(command,
|
||||||
sys.exit(1)
|
"Service '{}' needs to be built, but --no-build was passed.".format(
|
||||||
|
e.service.name), status=Status.FAILURE)
|
||||||
except NoSuchCommand as e:
|
except NoSuchCommand as e:
|
||||||
commands = "\n".join(parse_doc_section("commands:", getdoc(e.supercommand)))
|
commands = "\n".join(parse_doc_section("commands:", getdoc(e.supercommand)))
|
||||||
log.error("No such command: %s\n\n%s", e.command, commands)
|
exit_with_metrics(e.command, "No such command: {}\n\n{}".format(e.command, commands))
|
||||||
sys.exit(1)
|
|
||||||
except (errors.ConnectionError, StreamParseError):
|
except (errors.ConnectionError, StreamParseError):
|
||||||
sys.exit(1)
|
exit_with_metrics(command, status=Status.FAILURE)
|
||||||
|
except SystemExit as e:
|
||||||
|
status = Status.SUCCESS
|
||||||
|
if len(sys.argv) > 1 and '--help' not in sys.argv:
|
||||||
|
status = Status.FAILURE
|
||||||
|
|
||||||
|
if command and len(sys.argv) >= 3 and sys.argv[2] == '--help':
|
||||||
|
command = '--help ' + command
|
||||||
|
|
||||||
|
if not command and len(sys.argv) >= 2 and sys.argv[1] == '--help':
|
||||||
|
command = '--help'
|
||||||
|
|
||||||
|
msg = e.args[0] if len(e.args) else ""
|
||||||
|
code = 0
|
||||||
|
if isinstance(e.code, int):
|
||||||
|
code = e.code
|
||||||
|
exit_with_metrics(command, log_msg=msg, status=status,
|
||||||
|
exit_code=code)
|
||||||
|
|
||||||
|
|
||||||
|
def get_filtered_args(args):
|
||||||
|
if args[0] in ('-h', '--help'):
|
||||||
|
return []
|
||||||
|
if args[0] == '--version':
|
||||||
|
return ['version']
|
||||||
|
|
||||||
|
|
||||||
|
def exit_with_metrics(command, log_msg=None, status=Status.SUCCESS, exit_code=1):
|
||||||
|
if log_msg:
|
||||||
|
if not exit_code:
|
||||||
|
log.info(log_msg)
|
||||||
|
else:
|
||||||
|
log.error(log_msg)
|
||||||
|
|
||||||
|
MetricsCommand(command, status=status).send_metrics()
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|
||||||
def dispatch():
|
def dispatch():
|
||||||
@ -133,8 +177,10 @@ def setup_logging():
|
|||||||
root_logger.addHandler(console_handler)
|
root_logger.addHandler(console_handler)
|
||||||
root_logger.setLevel(logging.DEBUG)
|
root_logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Disable requests logging
|
# Disable requests and docker-py logging
|
||||||
|
logging.getLogger("urllib3").propagate = False
|
||||||
logging.getLogger("requests").propagate = False
|
logging.getLogger("requests").propagate = False
|
||||||
|
logging.getLogger("docker").propagate = False
|
||||||
|
|
||||||
|
|
||||||
def setup_parallel_logger(noansi):
|
def setup_parallel_logger(noansi):
|
||||||
@ -254,6 +300,7 @@ class TopLevelCommand:
|
|||||||
environment_file = self.toplevel_options.get('--env-file')
|
environment_file = self.toplevel_options.get('--env-file')
|
||||||
return Environment.from_env_file(self.project_dir, environment_file)
|
return Environment.from_env_file(self.project_dir, environment_file)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def build(self, options):
|
def build(self, options):
|
||||||
"""
|
"""
|
||||||
Build or rebuild services.
|
Build or rebuild services.
|
||||||
@ -305,6 +352,7 @@ class TopLevelCommand:
|
|||||||
progress=options.get('--progress'),
|
progress=options.get('--progress'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def config(self, options):
|
def config(self, options):
|
||||||
"""
|
"""
|
||||||
Validate and view the Compose file.
|
Validate and view the Compose file.
|
||||||
@ -354,6 +402,7 @@ class TopLevelCommand:
|
|||||||
|
|
||||||
print(serialize_config(compose_config, image_digests, not options['--no-interpolate']))
|
print(serialize_config(compose_config, image_digests, not options['--no-interpolate']))
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def create(self, options):
|
def create(self, options):
|
||||||
"""
|
"""
|
||||||
Creates containers for a service.
|
Creates containers for a service.
|
||||||
@ -382,6 +431,7 @@ class TopLevelCommand:
|
|||||||
do_build=build_action_from_opts(options),
|
do_build=build_action_from_opts(options),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def down(self, options):
|
def down(self, options):
|
||||||
"""
|
"""
|
||||||
Stops containers and removes containers, networks, volumes, and images
|
Stops containers and removes containers, networks, volumes, and images
|
||||||
@ -450,6 +500,7 @@ class TopLevelCommand:
|
|||||||
print(formatter(event))
|
print(formatter(event))
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
@metrics("exec")
|
||||||
def exec_command(self, options):
|
def exec_command(self, options):
|
||||||
"""
|
"""
|
||||||
Execute a command in a running container
|
Execute a command in a running container
|
||||||
@ -526,6 +577,7 @@ class TopLevelCommand:
|
|||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@metrics()
|
||||||
def help(cls, options):
|
def help(cls, options):
|
||||||
"""
|
"""
|
||||||
Get help on a command.
|
Get help on a command.
|
||||||
@ -539,6 +591,7 @@ class TopLevelCommand:
|
|||||||
|
|
||||||
print(getdoc(subject))
|
print(getdoc(subject))
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def images(self, options):
|
def images(self, options):
|
||||||
"""
|
"""
|
||||||
List images used by the created containers.
|
List images used by the created containers.
|
||||||
@ -593,6 +646,7 @@ class TopLevelCommand:
|
|||||||
])
|
])
|
||||||
print(Formatter.table(headers, rows))
|
print(Formatter.table(headers, rows))
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def kill(self, options):
|
def kill(self, options):
|
||||||
"""
|
"""
|
||||||
Force stop service containers.
|
Force stop service containers.
|
||||||
@ -607,6 +661,7 @@ class TopLevelCommand:
|
|||||||
|
|
||||||
self.project.kill(service_names=options['SERVICE'], signal=signal)
|
self.project.kill(service_names=options['SERVICE'], signal=signal)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def logs(self, options):
|
def logs(self, options):
|
||||||
"""
|
"""
|
||||||
View output from containers.
|
View output from containers.
|
||||||
@ -643,6 +698,7 @@ class TopLevelCommand:
|
|||||||
event_stream=self.project.events(service_names=options['SERVICE']),
|
event_stream=self.project.events(service_names=options['SERVICE']),
|
||||||
keep_prefix=not options['--no-log-prefix']).run()
|
keep_prefix=not options['--no-log-prefix']).run()
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def pause(self, options):
|
def pause(self, options):
|
||||||
"""
|
"""
|
||||||
Pause services.
|
Pause services.
|
||||||
@ -652,6 +708,7 @@ class TopLevelCommand:
|
|||||||
containers = self.project.pause(service_names=options['SERVICE'])
|
containers = self.project.pause(service_names=options['SERVICE'])
|
||||||
exit_if(not containers, 'No containers to pause', 1)
|
exit_if(not containers, 'No containers to pause', 1)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def port(self, options):
|
def port(self, options):
|
||||||
"""
|
"""
|
||||||
Print the public port for a port binding.
|
Print the public port for a port binding.
|
||||||
@ -673,6 +730,7 @@ class TopLevelCommand:
|
|||||||
options['PRIVATE_PORT'],
|
options['PRIVATE_PORT'],
|
||||||
protocol=options.get('--protocol') or 'tcp') or '')
|
protocol=options.get('--protocol') or 'tcp') or '')
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def ps(self, options):
|
def ps(self, options):
|
||||||
"""
|
"""
|
||||||
List containers.
|
List containers.
|
||||||
@ -729,6 +787,7 @@ class TopLevelCommand:
|
|||||||
])
|
])
|
||||||
print(Formatter.table(headers, rows))
|
print(Formatter.table(headers, rows))
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def pull(self, options):
|
def pull(self, options):
|
||||||
"""
|
"""
|
||||||
Pulls images for services defined in a Compose file, but does not start the containers.
|
Pulls images for services defined in a Compose file, but does not start the containers.
|
||||||
@ -752,6 +811,7 @@ class TopLevelCommand:
|
|||||||
include_deps=options.get('--include-deps'),
|
include_deps=options.get('--include-deps'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def push(self, options):
|
def push(self, options):
|
||||||
"""
|
"""
|
||||||
Pushes images for services.
|
Pushes images for services.
|
||||||
@ -766,6 +826,7 @@ class TopLevelCommand:
|
|||||||
ignore_push_failures=options.get('--ignore-push-failures')
|
ignore_push_failures=options.get('--ignore-push-failures')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def rm(self, options):
|
def rm(self, options):
|
||||||
"""
|
"""
|
||||||
Removes stopped service containers.
|
Removes stopped service containers.
|
||||||
@ -810,6 +871,7 @@ class TopLevelCommand:
|
|||||||
else:
|
else:
|
||||||
print("No stopped containers")
|
print("No stopped containers")
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def run(self, options):
|
def run(self, options):
|
||||||
"""
|
"""
|
||||||
Run a one-off command on a service.
|
Run a one-off command on a service.
|
||||||
@ -870,6 +932,7 @@ class TopLevelCommand:
|
|||||||
self.toplevel_options, self.toplevel_environment
|
self.toplevel_options, self.toplevel_environment
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def scale(self, options):
|
def scale(self, options):
|
||||||
"""
|
"""
|
||||||
Set number of containers to run for a service.
|
Set number of containers to run for a service.
|
||||||
@ -898,6 +961,7 @@ class TopLevelCommand:
|
|||||||
for service_name, num in parse_scale_args(options['SERVICE=NUM']).items():
|
for service_name, num in parse_scale_args(options['SERVICE=NUM']).items():
|
||||||
self.project.get_service(service_name).scale(num, timeout=timeout)
|
self.project.get_service(service_name).scale(num, timeout=timeout)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def start(self, options):
|
def start(self, options):
|
||||||
"""
|
"""
|
||||||
Start existing containers.
|
Start existing containers.
|
||||||
@ -907,6 +971,7 @@ class TopLevelCommand:
|
|||||||
containers = self.project.start(service_names=options['SERVICE'])
|
containers = self.project.start(service_names=options['SERVICE'])
|
||||||
exit_if(not containers, 'No containers to start', 1)
|
exit_if(not containers, 'No containers to start', 1)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def stop(self, options):
|
def stop(self, options):
|
||||||
"""
|
"""
|
||||||
Stop running containers without removing them.
|
Stop running containers without removing them.
|
||||||
@ -922,6 +987,7 @@ class TopLevelCommand:
|
|||||||
timeout = timeout_from_opts(options)
|
timeout = timeout_from_opts(options)
|
||||||
self.project.stop(service_names=options['SERVICE'], timeout=timeout)
|
self.project.stop(service_names=options['SERVICE'], timeout=timeout)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def restart(self, options):
|
def restart(self, options):
|
||||||
"""
|
"""
|
||||||
Restart running containers.
|
Restart running containers.
|
||||||
@ -936,6 +1002,7 @@ class TopLevelCommand:
|
|||||||
containers = self.project.restart(service_names=options['SERVICE'], timeout=timeout)
|
containers = self.project.restart(service_names=options['SERVICE'], timeout=timeout)
|
||||||
exit_if(not containers, 'No containers to restart', 1)
|
exit_if(not containers, 'No containers to restart', 1)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def top(self, options):
|
def top(self, options):
|
||||||
"""
|
"""
|
||||||
Display the running processes
|
Display the running processes
|
||||||
@ -963,6 +1030,7 @@ class TopLevelCommand:
|
|||||||
print(container.name)
|
print(container.name)
|
||||||
print(Formatter.table(headers, rows))
|
print(Formatter.table(headers, rows))
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def unpause(self, options):
|
def unpause(self, options):
|
||||||
"""
|
"""
|
||||||
Unpause services.
|
Unpause services.
|
||||||
@ -972,6 +1040,7 @@ class TopLevelCommand:
|
|||||||
containers = self.project.unpause(service_names=options['SERVICE'])
|
containers = self.project.unpause(service_names=options['SERVICE'])
|
||||||
exit_if(not containers, 'No containers to unpause', 1)
|
exit_if(not containers, 'No containers to unpause', 1)
|
||||||
|
|
||||||
|
@metrics()
|
||||||
def up(self, options):
|
def up(self, options):
|
||||||
"""
|
"""
|
||||||
Builds, (re)creates, starts, and attaches to containers for a service.
|
Builds, (re)creates, starts, and attaches to containers for a service.
|
||||||
@ -1122,6 +1191,7 @@ class TopLevelCommand:
|
|||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@metrics()
|
||||||
def version(cls, options):
|
def version(cls, options):
|
||||||
"""
|
"""
|
||||||
Show version information and quit.
|
Show version information and quit.
|
||||||
|
0
compose/metrics/__init__.py
Normal file
0
compose/metrics/__init__.py
Normal file
64
compose/metrics/client.py
Normal file
64
compose/metrics/client.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import os
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from docker import ContextAPI
|
||||||
|
from docker.transport import UnixHTTPAdapter
|
||||||
|
|
||||||
|
from compose.const import IS_WINDOWS_PLATFORM
|
||||||
|
|
||||||
|
if IS_WINDOWS_PLATFORM:
|
||||||
|
from docker.transport import NpipeHTTPAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class Status(Enum):
|
||||||
|
SUCCESS = "success"
|
||||||
|
FAILURE = "failure"
|
||||||
|
CANCELED = "canceled"
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsSource:
|
||||||
|
CLI = "docker-compose"
|
||||||
|
|
||||||
|
|
||||||
|
if IS_WINDOWS_PLATFORM:
|
||||||
|
METRICS_SOCKET_FILE = 'npipe://\\\\.\\pipe\\docker_cli'
|
||||||
|
else:
|
||||||
|
METRICS_SOCKET_FILE = 'http+unix:///var/run/docker-cli.sock'
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsCommand(requests.Session):
|
||||||
|
"""
|
||||||
|
Representation of a command in the metrics.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, command,
|
||||||
|
context_type=None, status=Status.SUCCESS,
|
||||||
|
source=MetricsSource.CLI, uri=None):
|
||||||
|
super().__init__()
|
||||||
|
self.command = "compose " + command if command else "compose --help"
|
||||||
|
self.context = context_type or ContextAPI.get_current_context().context_type or 'moby'
|
||||||
|
self.source = source
|
||||||
|
self.status = status.value
|
||||||
|
self.uri = uri or os.environ.get("METRICS_SOCKET_FILE", METRICS_SOCKET_FILE)
|
||||||
|
if IS_WINDOWS_PLATFORM:
|
||||||
|
self.mount("http+unix://", NpipeHTTPAdapter(self.uri))
|
||||||
|
else:
|
||||||
|
self.mount("http+unix://", UnixHTTPAdapter(self.uri))
|
||||||
|
|
||||||
|
def send_metrics(self):
|
||||||
|
try:
|
||||||
|
return self.post("http+unix://localhost/usage",
|
||||||
|
json=self.to_map(),
|
||||||
|
timeout=.05,
|
||||||
|
headers={'Content-Type': 'application/json'})
|
||||||
|
except Exception as e:
|
||||||
|
return e
|
||||||
|
|
||||||
|
def to_map(self):
|
||||||
|
return {
|
||||||
|
'command': self.command,
|
||||||
|
'context': self.context,
|
||||||
|
'source': self.source,
|
||||||
|
'status': self.status,
|
||||||
|
}
|
21
compose/metrics/decorator.py
Normal file
21
compose/metrics/decorator.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import functools
|
||||||
|
|
||||||
|
from compose.metrics.client import MetricsCommand
|
||||||
|
from compose.metrics.client import Status
|
||||||
|
|
||||||
|
|
||||||
|
class metrics:
|
||||||
|
def __init__(self, command_name=None):
|
||||||
|
self.command_name = command_name
|
||||||
|
|
||||||
|
def __call__(self, fn):
|
||||||
|
@functools.wraps(fn,
|
||||||
|
assigned=functools.WRAPPER_ASSIGNMENTS,
|
||||||
|
updated=functools.WRAPPER_UPDATES)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if not self.command_name:
|
||||||
|
self.command_name = fn.__name__
|
||||||
|
result = fn(*args, **kwargs)
|
||||||
|
MetricsCommand(self.command_name, status=Status.SUCCESS).send_metrics()
|
||||||
|
return result
|
||||||
|
return wrapper
|
@ -21,6 +21,7 @@ elif [ "$DOCKER_VERSIONS" == "all" ]; then
|
|||||||
DOCKER_VERSIONS=$($get_versions -n 2 recent)
|
DOCKER_VERSIONS=$($get_versions -n 2 recent)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
DOCKER_VERSIONS=19.03.14
|
||||||
|
|
||||||
BUILD_NUMBER=${BUILD_NUMBER-$USER}
|
BUILD_NUMBER=${BUILD_NUMBER-$USER}
|
||||||
PY_TEST_VERSIONS=${PY_TEST_VERSIONS:-py39}
|
PY_TEST_VERSIONS=${PY_TEST_VERSIONS:-py39}
|
||||||
|
@ -58,13 +58,16 @@ COMPOSE_COMPATIBILITY_DICT = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def start_process(base_dir, options):
|
def start_process(base_dir, options, executable=None, env=None):
|
||||||
|
executable = executable or DOCKER_COMPOSE_EXECUTABLE
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
[DOCKER_COMPOSE_EXECUTABLE] + options,
|
[executable] + options,
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
cwd=base_dir)
|
cwd=base_dir,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
print("Running process: %s" % proc.pid)
|
print("Running process: %s" % proc.pid)
|
||||||
return proc
|
return proc
|
||||||
|
|
||||||
@ -78,9 +81,10 @@ def wait_on_process(proc, returncode=0, stdin=None):
|
|||||||
return ProcessResult(stdout.decode('utf-8'), stderr.decode('utf-8'))
|
return ProcessResult(stdout.decode('utf-8'), stderr.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
def dispatch(base_dir, options, project_options=None, returncode=0, stdin=None):
|
def dispatch(base_dir, options,
|
||||||
|
project_options=None, returncode=0, stdin=None, executable=None, env=None):
|
||||||
project_options = project_options or []
|
project_options = project_options or []
|
||||||
proc = start_process(base_dir, project_options + options)
|
proc = start_process(base_dir, project_options + options, executable=executable, env=env)
|
||||||
return wait_on_process(proc, returncode=returncode, stdin=stdin)
|
return wait_on_process(proc, returncode=returncode, stdin=stdin)
|
||||||
|
|
||||||
|
|
||||||
|
125
tests/integration/metrics_test.py
Normal file
125
tests/integration/metrics_test.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
from http.server import BaseHTTPRequestHandler
|
||||||
|
from http.server import HTTPServer
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from docker.transport import UnixHTTPAdapter
|
||||||
|
|
||||||
|
from tests.acceptance.cli_test import dispatch
|
||||||
|
from tests.integration.testcases import DockerClientTestCase
|
||||||
|
|
||||||
|
|
||||||
|
TEST_SOCKET_FILE = '/tmp/test-metrics-docker-cli.sock'
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsTest(DockerClientTestCase):
|
||||||
|
test_session = requests.sessions.Session()
|
||||||
|
test_env = None
|
||||||
|
base_dir = 'tests/fixtures/v3-full'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
MetricsTest.test_session.mount("http+unix://", UnixHTTPAdapter(TEST_SOCKET_FILE))
|
||||||
|
MetricsTest.test_env = os.environ.copy()
|
||||||
|
MetricsTest.test_env['METRICS_SOCKET_FILE'] = TEST_SOCKET_FILE
|
||||||
|
MetricsServer().start()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def test_metrics_help(cls):
|
||||||
|
# root `docker-compose` command is considered as a `--help`
|
||||||
|
dispatch(cls.base_dir, [], env=MetricsTest.test_env)
|
||||||
|
assert cls.get_content() == \
|
||||||
|
b'{"command": "compose --help", "context": "moby", ' \
|
||||||
|
b'"source": "docker-compose", "status": "success"}'
|
||||||
|
dispatch(cls.base_dir, ['help', 'run'], env=MetricsTest.test_env)
|
||||||
|
assert cls.get_content() == \
|
||||||
|
b'{"command": "compose help", "context": "moby", ' \
|
||||||
|
b'"source": "docker-compose", "status": "success"}'
|
||||||
|
dispatch(cls.base_dir, ['--help'], env=MetricsTest.test_env)
|
||||||
|
assert cls.get_content() == \
|
||||||
|
b'{"command": "compose --help", "context": "moby", ' \
|
||||||
|
b'"source": "docker-compose", "status": "success"}'
|
||||||
|
dispatch(cls.base_dir, ['run', '--help'], env=MetricsTest.test_env)
|
||||||
|
assert cls.get_content() == \
|
||||||
|
b'{"command": "compose --help run", "context": "moby", ' \
|
||||||
|
b'"source": "docker-compose", "status": "success"}'
|
||||||
|
dispatch(cls.base_dir, ['up', '--help', 'extra_args'], env=MetricsTest.test_env)
|
||||||
|
assert cls.get_content() == \
|
||||||
|
b'{"command": "compose --help up", "context": "moby", ' \
|
||||||
|
b'"source": "docker-compose", "status": "success"}'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def test_metrics_simple_commands(cls):
|
||||||
|
dispatch(cls.base_dir, ['ps'], env=MetricsTest.test_env)
|
||||||
|
assert cls.get_content() == \
|
||||||
|
b'{"command": "compose ps", "context": "moby", ' \
|
||||||
|
b'"source": "docker-compose", "status": "success"}'
|
||||||
|
dispatch(cls.base_dir, ['version'], env=MetricsTest.test_env)
|
||||||
|
assert cls.get_content() == \
|
||||||
|
b'{"command": "compose version", "context": "moby", ' \
|
||||||
|
b'"source": "docker-compose", "status": "success"}'
|
||||||
|
dispatch(cls.base_dir, ['version', '--yyy'], env=MetricsTest.test_env)
|
||||||
|
assert cls.get_content() == \
|
||||||
|
b'{"command": "compose version", "context": "moby", ' \
|
||||||
|
b'"source": "docker-compose", "status": "failure"}'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_content():
|
||||||
|
resp = MetricsTest.test_session.get("http+unix://localhost")
|
||||||
|
print(resp.content)
|
||||||
|
return resp.content
|
||||||
|
|
||||||
|
|
||||||
|
def start_server(uri=TEST_SOCKET_FILE):
|
||||||
|
try:
|
||||||
|
os.remove(uri)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
httpd = HTTPServer(uri, MetricsHTTPRequestHandler, False)
|
||||||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
sock.bind(TEST_SOCKET_FILE)
|
||||||
|
sock.listen(0)
|
||||||
|
httpd.socket = sock
|
||||||
|
print('Serving on ', uri)
|
||||||
|
httpd.serve_forever()
|
||||||
|
sock.shutdown(socket.SHUT_RDWR)
|
||||||
|
sock.close()
|
||||||
|
os.remove(uri)
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsServer:
|
||||||
|
@classmethod
|
||||||
|
def start(cls):
|
||||||
|
t = Thread(target=start_server, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||||
|
usages = []
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
self.client_address = ('',) # avoid exception in BaseHTTPServer.py log_message()
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
for u in MetricsHTTPRequestHandler.usages:
|
||||||
|
self.wfile.write(u)
|
||||||
|
MetricsHTTPRequestHandler.usages = []
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
self.client_address = ('',) # avoid exception in BaseHTTPServer.py log_message()
|
||||||
|
content_length = int(self.headers['Content-Length'])
|
||||||
|
body = self.rfile.read(content_length)
|
||||||
|
print(body)
|
||||||
|
MetricsHTTPRequestHandler.usages.append(body)
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.getLogger("urllib3").propagate = False
|
||||||
|
logging.getLogger("requests").propagate = False
|
||||||
|
start_server()
|
@ -61,6 +61,7 @@ class DockerClientTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
|
cls.client.close()
|
||||||
del cls.client
|
del cls.client
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
0
tests/unit/metrics/__init__.py
Normal file
0
tests/unit/metrics/__init__.py
Normal file
36
tests/unit/metrics/metrics_test.py
Normal file
36
tests/unit/metrics/metrics_test.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from compose.metrics.client import MetricsCommand
|
||||||
|
from compose.metrics.client import Status
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsTest(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def test_metrics(cls):
|
||||||
|
assert MetricsCommand('up', 'moby').to_map() == {
|
||||||
|
'command': 'compose up',
|
||||||
|
'context': 'moby',
|
||||||
|
'status': 'success',
|
||||||
|
'source': 'docker-compose',
|
||||||
|
}
|
||||||
|
|
||||||
|
assert MetricsCommand('down', 'local').to_map() == {
|
||||||
|
'command': 'compose down',
|
||||||
|
'context': 'local',
|
||||||
|
'status': 'success',
|
||||||
|
'source': 'docker-compose',
|
||||||
|
}
|
||||||
|
|
||||||
|
assert MetricsCommand('help', 'aci', Status.FAILURE).to_map() == {
|
||||||
|
'command': 'compose help',
|
||||||
|
'context': 'aci',
|
||||||
|
'status': 'failure',
|
||||||
|
'source': 'docker-compose',
|
||||||
|
}
|
||||||
|
|
||||||
|
assert MetricsCommand('run', 'ecs').to_map() == {
|
||||||
|
'command': 'compose run',
|
||||||
|
'context': 'ecs',
|
||||||
|
'status': 'success',
|
||||||
|
'source': 'docker-compose',
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user