Skip to content

Commit

Permalink
chore: improved logging (#14)
Browse files Browse the repository at this point in the history
* chore: improved logging

* fix: added log_level parameter to action

* docs: Update README.md
  • Loading branch information
mbruzda-splunk committed Aug 2, 2023
1 parent a43d6b6 commit 46da6de
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 107 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
| `app_path` | Path to the directory where addon is located, without filename | **required** | |
| `included_tags` | Comma separated list of [tags](#reference-docs) to include in appinspect job | | None |
| `excluded_tags` | Comma separated list of [tags](#reference-docs) to exclude from appinspect job | | None |
| `log_level` | Python logging level for action | | `INFO` |

You can explicitly include and exclude tags from a validation by including additional options in your request. Specifically, using the included_tags and excluded_tags options includes and excludes the tags you specify from a validation. If no tags are specified all checks will be done and no tags are excluded from the validation.

Expand Down
6 changes: 5 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ inputs:
description: comma seperated list of tags to be excluded from appinspect scans
default: ""
required: false
log_level:
description: severity for python logging ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
default: "INFO"
required: false
runs:
using: "docker"
image: docker://ghcr.io/splunk/appinspect-api-action/appinspect-api-action:v3.0.0
image: Dockerfile
4 changes: 2 additions & 2 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
ADDON_NAME=$(ls $INPUT_APP_PATH)
ADDON_FULL_PATH="$INPUT_APP_PATH/$ADDON_NAME"

echo "$INPUT_USERNAME" "$INPUT_PASSWORD" "$ADDON_FULL_PATH" "$INPUT_INCLUDED_TAGS" "$INPUT_EXCLUDED_TAGS"
echo "$INPUT_USERNAME" "$INPUT_PASSWORD" "$ADDON_FULL_PATH" "$INPUT_INCLUDED_TAGS" "$INPUT_EXCLUDED_TAGS" "$INPUT_LOG_LEVEL"

python3 /main.py "$INPUT_USERNAME" "$INPUT_PASSWORD" "$ADDON_FULL_PATH" "$INPUT_INCLUDED_TAGS" "$INPUT_EXCLUDED_TAGS"
python3 /main.py "$INPUT_USERNAME" "$INPUT_PASSWORD" "$ADDON_FULL_PATH" "$INPUT_INCLUDED_TAGS" "$INPUT_EXCLUDED_TAGS" "$INPUT_LOG_LEVEL"
73 changes: 50 additions & 23 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import random
import json
import yaml
import logging

from pathlib import Path
from typing import Dict, Any, Tuple, Callable, Sequence, Optional, List
Expand Down Expand Up @@ -46,10 +47,12 @@ def _retry_request(
for retry_num in range(num_retries):
if retry_num > 0:
sleep_time = rand() + retry_num
print(
logging.info(
f"Sleeping {sleep_time} seconds before retry "
f"{retry_num} of {num_retries - 1} after {reason}"
f"{retry_num} of {num_retries - 1}"
)
if reason:
logging.info(reason)
sleep(sleep_time)
response = requests.request(
method,
Expand All @@ -70,7 +73,7 @@ def _retry_request(
reason = f"response status code: {response.status_code}, for message: {error_message}"
continue
if not validation_function(response):
print("Response did not pass the validation, retrying...")
logging.info("Response did not pass the validation, retrying...")
continue
return response
raise CouldNotRetryRequestException()
Expand All @@ -93,17 +96,18 @@ def _download_report(


def login(username: str, password: str) -> requests.Response:
logging.debug("Sending request to retrieve login token")
try:
return _retry_request(
"GET",
"https://api.splunk.com/2.0/rest/login/splunk",
auth=(username, password),
)
except CouldNotAuthenticateException:
print("Credentials are not correct, please check the configuration.")
logging.error("Credentials are not correct, please check the configuration.")
sys.exit(1)
except CouldNotRetryRequestException:
print("Could not get response after all retries, exiting...")
logging.error("Could not get response after all retries, exiting...")
sys.exit(1)


Expand All @@ -114,6 +118,7 @@ def validate(token: str, build: Path, payload: Dict[str, str]) -> requests.Respo
(build.name, open(build.as_posix(), "rb"), "application/octet-stream"),
)
]
logging.debug(f"Sending package `{build.name}` for validation")
try:
response = _retry_request(
"POST",
Expand All @@ -126,22 +131,28 @@ def validate(token: str, build: Path, payload: Dict[str, str]) -> requests.Respo
)
return response
except CouldNotAuthenticateException:
print("Credentials are not correct, please check the configuration.")
logging.error("Credentials are not correct, please check the configuration.")
sys.exit(1)
except CouldNotRetryRequestException:
print("Could not get response after all retries, exiting...")
logging.error("Could not get response after all retries, exiting...")
sys.exit(1)


def submit(token: str, request_id: str) -> requests.Response:
def _validate_validation_status(response: requests.Response) -> bool:
return response.json()["status"] == "SUCCESS"
is_successful = response.json()["status"] == "SUCCESS"
if is_successful:
logging.debug(
f'Response status is `{response.json()["status"]}`, "SUCCESS" expected.'
)
return is_successful

# appinspect api needs some time to process the request
# if the response status will be "PROCESSING" wait 60s and make another call

# there is a problem with pycov marking this line as not covered - excluded from coverage
try:
logging.debug("Submitting package")
return _retry_request( # pragma: no cover
"GET",
f"https://appinspect.splunk.com/v1/app/validate/status/{request_id}",
Expand All @@ -153,22 +164,24 @@ def _validate_validation_status(response: requests.Response) -> bool:
validation_function=_validate_validation_status,
)
except CouldNotAuthenticateException:
print("Credentials are not correct, please check the configuration.")
logging.error("Credentials are not correct, please check the configuration.")
sys.exit(1)
except CouldNotRetryRequestException:
print("Could not get response after all retries, exiting...")
logging.error("Could not get response after all retries, exiting...")
sys.exit(1)


def download_json_report(
token: str, request_id: str, payload: Dict[str, Any]
) -> requests.Response:
logging.debug("Downloading response in json format")
return _download_report(
token=token, request_id=request_id, payload=payload, response_type="json"
)


def download_and_save_html_report(token: str, request_id: str, payload: Dict[str, Any]):
logging.debug("Downloading report in html format")
response = _download_report(
token=token, request_id=request_id, payload=payload, response_type="html"
)
Expand All @@ -178,6 +191,7 @@ def download_and_save_html_report(token: str, request_id: str, payload: Dict[str


def get_appinspect_failures_list(response_dict: Dict[str, Any]) -> List[str]:
logging.debug("Parsing json response to find failed checks\n")
reports = response_dict["reports"]
groups = reports[0]["groups"]

Expand All @@ -187,7 +201,7 @@ def get_appinspect_failures_list(response_dict: Dict[str, Any]) -> List[str]:
for check in group["checks"]:
if check["result"] == "failure":
failed_tests_list.append(check["name"])
print(f"Failed appinspect check for name: {check['name']}\n")
logging.debug(f"Failed appinspect check for name: {check['name']}\n")
return failed_tests_list


Expand All @@ -196,26 +210,24 @@ def read_yaml_as_dict(filename_path: Path) -> Dict[str, str]:
try:
out_dict = yaml.safe_load(file)
except yaml.YAMLError as e:
print(f"Can not read yaml file named {filename_path}")
logging.error(f"Can not read yaml file named {filename_path}")
raise e
return out_dict if out_dict else {}


def compare_failures(failures: List[str], expected: List[str]):
if sorted(failures) != sorted(expected):
print(
"Appinspect failures doesn't match appinspect.expect file, check for exceptions file"
)
logging.debug(f"Appinspect failures: {failures}")
logging.debug(f"Expected failures: {expected}")
raise AppinspectFailures


def parse_results(results: Dict[str, Any]):
print(results)
print("\n======== AppInspect Api Results ========")
for metric, count in results["info"].items():
print(f"{metric:>15} : {count: <4}")
if results["info"]["error"] > 0 or results["info"]["failure"] > 0:
print("\nError or failures found in App Inspect\n")
logging.warning("Error or failures found in AppInspect Report")
raise AppinspectChecksFailuresException


Expand All @@ -230,14 +242,23 @@ def build_payload(included_tags: str, excluded_tags: str) -> Dict[str, str]:


def compare_against_known_failures(response_json: Dict[str, Any], exceptions_file_path):
logging.info(
f"Comparing AppInspect Failures with `{exceptions_file_path.name}` file"
)
failures = get_appinspect_failures_list(response_json)

if exceptions_file_path.exists():
expected_failures = list(read_yaml_as_dict(exceptions_file_path).keys())
compare_failures(failures, expected_failures)
try:
compare_failures(failures, expected_failures)
except AppinspectFailures:
logging.error(
"Appinspect failures don't match appinspect.expect file, check for exceptions file"
)
sys.exit(1)
else:
print(
f"ERROR: File `{exceptions_file_path.name}` not found, please create `{exceptions_file_path.name}` file with exceptions\n" # noqa: E501
logging.error(
f"File `{exceptions_file_path.name}` not found, please create `{exceptions_file_path.name}` file with exceptions\n" # noqa: E501
)
sys.exit(1)

Expand All @@ -251,27 +272,33 @@ def main(argv: Optional[Sequence[str]] = None):
parser.add_argument("app_path")
parser.add_argument("included_tags")
parser.add_argument("excluded_tags")
parser.add_argument("log_level")

appinspect_expect_filename = ".appinspect_api.expect.yaml"

args = parser.parse_args(argv)

print(
logging.basicConfig(level=args.log_level)

logging.info(
f"app_path={args.app_path}, included_tags={args.included_tags}, excluded_tags={args.excluded_tags}"
)
build = Path(args.app_path)

login_response = login(args.username, args.password)
token = login_response.json()["data"]["token"]
print("Successfully received token")
logging.debug("Successfully received token")

payload = build_payload(args.included_tags, args.excluded_tags)
logging.debug(f"Validation payload: {payload}")

validate_response = validate(token, build, payload)
print(f"Successfully sent package for validation using {payload}")
logging.debug(f"Successfully sent package for validation using {payload}")
request_id = validate_response.json()["request_id"]

submit_response = submit(token, request_id)
logging.info("Successfully submitted and validated package")

download_and_save_html_report(token, request_id, payload)

# if this is true it compares the exceptions and results
Expand Down
Loading

0 comments on commit 46da6de

Please sign in to comment.