diff --git a/README.md b/README.md index ac68205..8e0504f 100644 --- a/README.md +++ b/README.md @@ -34,23 +34,24 @@ sudo docker run --rm -i \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /host/path:/ci/output:rw `#Optional, will contain all the files the container creates.` \ -e IMAGE="linuxserver/" \ --e TAGS="" \ +-e TAGS="" \ -e META_TAG= \ -e BASE= \ -e SECRET_KEY= \ -e ACCESS_KEY= \ --e DOCKER_ENV="" \ +-e DOCKER_ENV="" \ -e WEB_AUTH="" \ -e WEB_PATH=". Defaults to ''." \ -e S3_REGION= \ -e S3_BUCKET= \ --e WEB_SCREENSHOT_DELAY= +-e WEB_SCREENSHOT_TIMEOUT= +-e SBOM_TIMEOUT= -e WEB_SCREENSHOT= \ --e DELAY_START= \ -e PORT= \ -e SSL= \ -e CI_S6_VERBOSITY= \ --e DOCKER_LOGS_DELAY= \ +-e CI_LOG_LEVEL= \ +-e DOCKER_LOGS_TIMEOUT= \ -e DRY_RUN= -t lsiodev/ci:latest \ python3 test_build.py diff --git a/ci/ci.py b/ci/ci.py index a474020..855b408 100755 --- a/ci/ci.py +++ b/ci/ci.py @@ -102,6 +102,8 @@ class SetEnvs(): META_TAG: '{os.environ.get("META_TAG")}' TAGS: '{os.environ.get("TAGS")}' S6_VERBOSITY: '{os.environ.get("S6_VERBOSITY")}' + CI_S6_VERBOSITY '{os.environ.get("CI_S6_VERBOSITY")}' + CI_LOG_LEVEL '{os.environ.get("CI_LOG_LEVEL")}' DOCKER_ENV: '{os.environ.get("DOCKER_ENV")}' DOCKER_VOLUMES: '{os.environ.get("DOCKER_VOLUMES")}' (Not in use) DOCKER_PRIVILEGED: '{os.environ.get("DOCKER_PRIVILEGED")}' (Not in use) @@ -119,7 +121,7 @@ class SetEnvs(): S3_REGION: '{os.environ.get("S3_REGION")}' S3_BUCKET: '{os.environ.get("S3_BUCKET")}' """) - logger.info(env_data) + self.logger.info(env_data) def _split_key_value_string(self, kv:str, make_list:bool = False) -> dict[str,str] | list[str]: """Split a key value string into a dictionary or list. @@ -165,7 +167,7 @@ class SetEnvs(): volumes (str, optional): A string with key values separated by the pipe symbol. e.g `key1=val1|key2=val2`. Defaults to None. Raises: - CIError: Raises a CIError Exception if it failes to parse the string + CIError: Raises a CIError Exception if it fails to parse the string Returns: list[str]: Returns a list with our keys and values. @@ -184,7 +186,7 @@ class SetEnvs(): """Make sure all needed ENVs are set Raises: - CIError: Raises a CIError exception if one of the enviroment values is not set. + CIError: Raises a CIError exception if one of the environment values is not set. """ try: self.image: str = os.environ["IMAGE"] @@ -211,12 +213,12 @@ class CI(SetEnvs): s3_client (boto3.client): S3 client object Args: - SetEnvs (Object): Helper class that initializes and checks that all the necessary enviroment variables exists. Object is initialized upon init of CI. + SetEnvs (Object): Helper class that initializes and checks that all the necessary environment variables exists. Object is initialized upon init of CI. """ def __init__(self) -> None: super().__init__() # Init the SetEnvs object. self.logger = logging.getLogger("LSIO CI") - logging.getLogger("botocore.auth").setLevel(logging.INFO) # Don"t log the S3 authentication steps. + logging.getLogger("botocore.auth").setLevel(logging.INFO) # Don't log the S3 authentication steps. self.client: DockerClient = docker.from_env() self.tags = list(self.tags_env.split("|")) @@ -291,7 +293,7 @@ class CI(SetEnvs): self.take_screenshot(container, tag) self._endtest(container, tag, build_info, sbom, True) - self.logger.info("Test of %s PASSED after %.2f seconds", tag, time.time() - start_time) + self.logger.success("Test of %s PASSED after %.2f seconds", tag, time.time() - start_time) return def _endtest(self, container:Container, tag:str, build_info:dict[str,str], packages:str, test_success: bool) -> None: @@ -412,7 +414,7 @@ class CI(SetEnvs): if "VERSION" in logblob: self.logger.info("Get package versions for %s completed", tag) self._add_test_result(tag, test, "PASS", "-") - self.logger.info("%s package list %s: PASS", test, tag) + self.logger.success("%s package list %s: PASS", test, tag) self.create_html_ansi_file(str(logblob),tag,"sbom") try: syft.remove(force=True) @@ -489,7 +491,7 @@ class CI(SetEnvs): "maintainer": container.attrs["Config"]["Labels"]["maintainer"], } self._add_test_result(tag, test, "PASS", "-") - self.logger.info("Get build info on tag '%s': PASS", tag) + self.logger.success("Get build info on tag '%s': PASS", tag) except (APIError,KeyError) as error: self.logger.exception("Get build info on tag '%s': FAIL", tag) build_info = {"version": "ERROR", "created": "ERROR", "size": "ERROR", "maintainer": "ERROR"} @@ -519,7 +521,7 @@ class CI(SetEnvs): if "[services.d] done." in logblob or "[ls.io-init] done." in logblob: self.logger.info("%s completed for %s",test, tag) self._add_test_result(tag, test, "PASS", "-") - self.logger.info("%s %s: PASS", test, tag) + self.logger.success("%s %s: PASS", test, tag) return True time.sleep(1) except APIError as error: @@ -611,7 +613,7 @@ class CI(SetEnvs): """ try: - self.logger.info(f"Creating {tag}.{name}.html") + self.logger.info("Creating %s.%s.html", tag, name) converter = Ansi2HTMLConverter(title=f"{tag}-{name}") html_logs: str = converter.convert(blob,full=full) with open(f"{self.outdir}/{tag}.{name}.html", "w", encoding="utf-8") as file: @@ -683,6 +685,7 @@ class CI(SetEnvs): screenshot_timeout = time.time() + int(self.screenshot_timeout) test = "Get screenshot" try: + self.logger.info("Trying for %s seconds to take a screenshot of %s ",self.screenshot_timeout, tag) driver: WebDriver = self.setup_driver() while time.time() < screenshot_timeout: try: @@ -690,12 +693,13 @@ class CI(SetEnvs): ip_adr:str = container.attrs.get("NetworkSettings",{}).get("Networks",{}).get("bridge",{}).get("IPAddress","") endpoint: str = f"{proto}://{self.webauth}@{ip_adr}:{self.port}{self.webpath}" driver.get(endpoint) - self.logger.info("Trying to take screenshot of %s at %s", tag, endpoint) + self.logger.debug("Trying to take screenshot of %s at %s", tag, endpoint) driver.get_screenshot_as_file(f"{self.outdir}/{tag}.png") if not os.path.isfile(f"{self.outdir}/{tag}.png"): continue self._add_test_result(tag, test, "PASS", "-") - self.logger.info("Screenshot %s: PASS", tag) + self.logger.success("Screenshot %s: PASS", tag) + return except Exception as error: logger.debug("Failed to take screenshot of %s at %s, trying again in 1 second", tag, endpoint) logger.debug("Error: %s", error) diff --git a/ci/logger.py b/ci/logger.py index 25fb83c..e03fcc7 100644 --- a/ci/logger.py +++ b/ci/logger.py @@ -19,12 +19,30 @@ else: logger: Logger = logging.getLogger() +# Add custom log level for success messages +logging.SUCCESS = 25 +logging.addLevelName(logging.SUCCESS, "SUCCESS") + +def success(self:'Logger', message:str, *args, **kwargs): + """Log 'message % args' with severity 'SUCCESS'. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.success("Houston, Tranquility Base Here. The Eagle has Landed.", exc_info=1) + """ + if self.isEnabledFor(logging.SUCCESS): + self._log(logging.SUCCESS, message, args, **kwargs) + +logging.Logger.success = success + class ColorPercentStyle(logging.PercentStyle): """Custom log formatter that add color to specific log levels.""" grey: str = "38" yellow: str = "33" red: str = "31" cyan: str = "36" + green: str = "32" def _get_color_fmt(self, color_code, bold=False) -> str: if bold: @@ -37,7 +55,8 @@ class ColorPercentStyle(logging.PercentStyle): logging.INFO: self._get_color_fmt(self.cyan), logging.WARNING: self._get_color_fmt(self.yellow), logging.ERROR: self._get_color_fmt(self.red), - logging.CRITICAL: self._get_color_fmt(self.red) + logging.CRITICAL: self._get_color_fmt(self.red), + logging.SUCCESS: self._get_color_fmt(self.green) } return colors.get(levelno, self._get_color_fmt(self.grey)) diff --git a/ci/template.html b/ci/template.html index e2685e6..def8741 100644 --- a/ci/template.html +++ b/ci/template.html @@ -131,7 +131,8 @@ .log-debug {color:lightgray} .log-info {color:lightskyblue} .log-warning {color:darkorange} - .log-error {color:red} + .log-error {color:red;font-weight: bolder;} + .log-success{color:limegreen;font-weight: bolder;} } @media (prefers-color-scheme: light) { @@ -212,7 +213,8 @@ .log-debug {color:#9bb0bf} .log-info {color:#60707c} .log-warning {color:darkorange} - .log-error {color:red} + .log-error {color:red;font-weight: bolder;} + .log-success{color:#009879;font-weight: bolder;} } body, @@ -662,7 +664,8 @@ pylogs = logs.replace(/\[38;20m/gi,"" ).replace(/\[33;20m/gi,"" ).replace(/\[31;20m/gi,"" - ).replace(/\[36;20m/gi,"" + ).replace(/\[36;20m/gi,"" + ).replace(/\[32;20m/gi,"" ).replace(/\[0m/gi,"") document.getElementById("logs").innerHTML = pylogs }) diff --git a/test_build.py b/test_build.py index 1a9cfac..cb8fab0 100644 --- a/test_build.py +++ b/test_build.py @@ -1,22 +1,26 @@ #!/usr/bin/env python3 import os +import time from logging import Logger from ci.ci import CI, CIError from ci.logger import configure_logging def run_test() -> None: """Run tests on container tags then build and upload reports""" + start_time = time.time() ci.run(ci.tags) # Don't set the whole report as failed if any of the ARM tag fails. for tag in ci.report_containers.keys(): if tag.startswith("amd64") and ci.report_containers[tag]['test_success'] == True: ci.report_status = 'PASS' # Override the report_status if an ARM tag failed, but the amd64 tag passed. + if ci.report_status == 'PASS': + logger.success('All tests PASSED after %.2f seconds', time.time() - start_time) + ci.report_render() ci.badge_render() ci.json_render() ci.report_upload() if ci.report_status == 'PASS': # Exit based on test results - logger.info('Tests PASSED') ci.log_upload() return logger.error('Tests FAILED')