Skip to content
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

chore: improved logging #14

Merged
merged 16 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading