-
Notifications
You must be signed in to change notification settings - Fork 97
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
Generate QHub Costs via infracost
#1340
Changes from 4 commits
35f64b2
01722a5
180d7d0
265be90
b247dec
edf6c9a
d1c7b8a
4088158
1850117
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import logging | ||
|
||
from qhub.cost import infracost_report | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def create_cost_subcommand(subparser): | ||
subparser = subparser.add_parser("cost-estimate") | ||
subparser.add_argument( | ||
"-p", | ||
"--path", | ||
help="Pass the path of your stages directory generated after rendering QHub configurations before deployment", | ||
required=False, | ||
) | ||
subparser.set_defaults(func=handle_cost_report) | ||
|
||
|
||
def handle_cost_report(args): | ||
infracost_report(args.path) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import json | ||
import logging | ||
import os | ||
import re | ||
import subprocess | ||
|
||
from rich.console import Console | ||
from rich.table import Table | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def _check_infracost(): | ||
""" | ||
Check if infracost is installed | ||
""" | ||
try: | ||
subprocess.check_output(["infracost", "--version"]) | ||
return True | ||
except subprocess.CalledProcessError: | ||
return False | ||
|
||
|
||
def _check_infracost_api_key(): | ||
""" | ||
Check if infracost API key is configured | ||
""" | ||
try: | ||
subprocess.check_output(["infracost", "configure", "get", "api_key"]) | ||
return True | ||
except subprocess.CalledProcessError: | ||
return False | ||
|
||
|
||
def _run_infracost(path): | ||
""" | ||
Run infracost on the given path and return the JSON output | ||
""" | ||
try: | ||
process = subprocess.Popen( | ||
["infracost", "breakdown", "--path", path, "--format", "json"], | ||
stdout=subprocess.PIPE, | ||
stderr=subprocess.PIPE, | ||
) | ||
stdout, stderr = process.communicate() | ||
infracost_data = json.loads( | ||
re.search("({.+})", stdout.decode("UTF-8")) | ||
.group(0) | ||
.replace("u'", '"') | ||
.replace("'", '"') | ||
) | ||
return infracost_data | ||
except subprocess.CalledProcessError: | ||
return None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe a small note here about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you want a comment or an error message? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we explain what went wrong so perhaps an error message. |
||
except AttributeError: | ||
return None | ||
|
||
|
||
def infracost_report(path): | ||
""" | ||
Generate a report of the infracost cost of the given path | ||
args: | ||
path: path to the qhub stages directory | ||
""" | ||
if not path: | ||
path = os.path.join(os.getcwd(), "stages") | ||
|
||
if _check_infracost() and _check_infracost_api_key(): | ||
if not os.path.exists(path): | ||
print("Deployment is not available") | ||
else: | ||
data = _run_infracost(path) | ||
if data: | ||
cost_table = Table(title="Cost Breakdown") | ||
cost_table.add_column( | ||
"Name", justify="right", style="cyan", no_wrap=True | ||
) | ||
cost_table.add_column( | ||
"Cost ($)", justify="right", style="cyan", no_wrap=True | ||
) | ||
|
||
cost_table.add_row("Total Monthly Cost", data["totalMonthlyCost"]) | ||
cost_table.add_row("Total Hourly Cost", data["totalHourlyCost"]) | ||
|
||
resource_table = Table(title="Resource Breakdown") | ||
resource_table.add_column( | ||
"Name", justify="right", style="cyan", no_wrap=True | ||
) | ||
resource_table.add_column( | ||
"Number", justify="right", style="cyan", no_wrap=True | ||
) | ||
|
||
resource_table.add_row( | ||
"Total Detected Costs", | ||
str(data["summary"]["totalDetectedResources"]), | ||
) | ||
resource_table.add_row( | ||
"Total Supported Resources", | ||
str(data["summary"]["totalSupportedResources"]), | ||
) | ||
resource_table.add_row( | ||
"Total Un-Supported Resources", | ||
str(data["summary"]["totalUnsupportedResources"]), | ||
) | ||
resource_table.add_row( | ||
"Total Non-Priced Resources", | ||
str(data["summary"]["totalNoPriceResources"]), | ||
) | ||
resource_table.add_row( | ||
"Total Usage-Priced Resources", | ||
str(data["summary"]["totalUsageBasedResources"]), | ||
) | ||
|
||
console = Console() | ||
console.print(cost_table) | ||
console.print(resource_table) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be neat to add the ability to write to csv and/or png, but that's just an idea. I don't want to hold up this PR for that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that we are making an online accessible dashboard available to them, I don't think giving them a CSV feature would be much useful. However making them JSON available to compare with previous cost reports would be something I would work on next 👍 |
||
try: | ||
print(f"Access the dashboard here: {data['shareUrl']}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't these print statements be logger.info statements? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess its not a log statement but rather should be showcased on the CLI interface. Hence it needs to be a print statement in my opinion. |
||
except KeyError: | ||
print( | ||
"Dashboard is not available. Enable it via: infracost configure set enable_dashboard true" | ||
) | ||
else: | ||
print( | ||
"No data was generated. Please check your QHub configuration and generated stages." | ||
) | ||
else: | ||
print("Infracost is not installed or the API key is not configured") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a little more info to the
help
command regarding what the differentResource Breakdown
categories means?