From d79834a18220b6b9552e73e93cbf79a54717c0ed Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Tue, 1 Oct 2024 14:47:11 +0530 Subject: [PATCH] Add a command to handle deployment auctions --- .../webapp/deploy_webapp_from_registry.py | 7 +- .../webapp/handle_deployment_auction.py | 162 ++++++++++++++++++ .../webapp/publish_deployment_auction.py | 2 + .../webapp/request_webapp_deployment.py | 11 +- stack_orchestrator/deploy/webapp/util.py | 19 +- stack_orchestrator/main.py | 2 + 6 files changed, 192 insertions(+), 11 deletions(-) create mode 100644 stack_orchestrator/deploy/webapp/handle_deployment_auction.py diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py index aa3a4d8f..3e69280a 100644 --- a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py @@ -42,6 +42,7 @@ from stack_orchestrator.deploy.webapp.util import ( match_owner, skip_by_tag, confirm_payment, + load_known_requests, ) @@ -257,12 +258,6 @@ def process_app_deployment_request( logger.log("END - process_app_deployment_request") -def load_known_requests(filename): - if filename and os.path.exists(filename): - return json.load(open(filename, "r")) - return {} - - def dump_known_requests(filename, requests, status="SEEN"): if not filename: return diff --git a/stack_orchestrator/deploy/webapp/handle_deployment_auction.py b/stack_orchestrator/deploy/webapp/handle_deployment_auction.py new file mode 100644 index 00000000..950a468a --- /dev/null +++ b/stack_orchestrator/deploy/webapp/handle_deployment_auction.py @@ -0,0 +1,162 @@ +# Copyright ©2023 Vulcanize +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import sys +import json + +import click + +from stack_orchestrator.deploy.webapp.util import ( + AttrDict, + LaconicRegistryClient, + TimedLogger, + load_known_requests, +) + + +def process_app_deployment_auction( + ctx, + laconic: LaconicRegistryClient, + request, + current_status, + bid_amount, + logger, +): + logger.log("BEGIN - process_app_deployment_auction") + status = current_status + + # TODO: + # Check max_price, skip if bid_amount > max_price + # Check auction status + # Commit bid if auction in commit state + # Reveal bid if auction in reveal state + + logger.log("END - process_app_deployment_auction") + return status, "" + + +def dump_known_auction_requests(filename, requests, status="SEEN"): + if not filename: + return + known_requests = load_known_requests(filename) + for r in requests: + known_requests[r.id] = {"revealFile": r.revealFile, "status": status} + with open(filename, "w") as f: + json.dump(known_requests, f) + + +@click.command() +@click.option( + "--laconic-config", help="Provide a config file for laconicd", required=True +) +@click.option( + "--state-file", help="File to store state about previously seen auction requests." +) +@click.option( + "--bid-amount", + help="Bid to place on application deployment auctions (in alnt)", + required=True, +) +@click.option( + "--dry-run", help="Don't do anything, just report what would be done.", is_flag=True +) +@click.pass_context +def command( + ctx, + laconic_config, + state_file, + bid_amount, + dry_run, +): # noqa: C901 + if bid_amount < 0: + print("--bid-amount cannot be less than 0", file=sys.stderr) + sys.exit(2) + + logger = TimedLogger(file=sys.stderr) + + try: + laconic = LaconicRegistryClient(laconic_config, log_file=sys.stderr) + auctions_requests = laconic.app_deployment_auctions() + + previous_requests = {} + if state_file: + logger.log(f"Loading known auctions from {state_file}...") + previous_requests = load_known_requests(state_file) + + # Process new requests first + auctions_requests.sort(key=lambda r: r.createTime) + auctions_requests.reverse() + + requests_to_execute = [] + + for r in auctions_requests: + logger.log(f"BEGIN: Examining request {r.id}") + result_status = "PENDING" + try: + application = r.attributes.application + + # Handle already seen requests + if r.id in previous_requests: + # If it's not in commit or reveal status, skip the request as we've already seen it + current_status = previous_requests[r.id].get("status", "") + result_status = current_status + if current_status not in ["COMMIT", "REVEAL"]: + logger.log(f"Skipping request {r.id}, we've already seen it.") + continue + + logger.log(f"Found existing auction {r.id} for application ${application}, status {current_status}.") + else: + # It's a fresh request, check application record + app = laconic.get_record(application) + if not app: + logger.log(f"Skipping request {r.id}, cannot locate app.") + result_status = "ERROR" + continue + + logger.log(f"Found pending auction request {r.id} for application {application}.") + + # Add requests to be processed + requests_to_execute.append((r, result_status)) + + except Exception as e: + result_status = "ERROR" + logger.log(f"ERROR examining request {r.id}: " + str(e)) + finally: + logger.log(f"DONE Examining request {r.id} with result {result_status}.") + if result_status in ["ERROR"]: + dump_known_auction_requests(state_file, [AttrDict({"id": r.id, "revealFile": ""})], status=result_status) + + logger.log(f"Found {len(requests_to_execute)} request(s) to process.") + + if not dry_run: + for r, current_status in requests_to_execute: + logger.log(f"Processing {r.id}: BEGIN") + result_status = "ERROR" + try: + result_status, reveal_file_path = process_app_deployment_auction( + ctx, + laconic, + r, + current_status, + bid_amount, + logger, + ) + except Exception as e: + logger.log(f"ERROR {r.id}:" + str(e)) + finally: + logger.log(f"Processing {r.id}: END - {result_status}") + dump_known_auction_requests(state_file, [AttrDict({"id": r.id, "revealFile": reveal_file_path})], result_status) + except Exception as e: + logger.log("UNCAUGHT ERROR:" + str(e)) + raise e diff --git a/stack_orchestrator/deploy/webapp/publish_deployment_auction.py b/stack_orchestrator/deploy/webapp/publish_deployment_auction.py index 36b2e710..b6694638 100644 --- a/stack_orchestrator/deploy/webapp/publish_deployment_auction.py +++ b/stack_orchestrator/deploy/webapp/publish_deployment_auction.py @@ -25,10 +25,12 @@ from stack_orchestrator.deploy.webapp.util import ( LaconicRegistryClient, ) + def fatal(msg: str): print(msg, file=sys.stderr) sys.exit(1) + # TODO: Add defaults for auction params @click.command() @click.option( diff --git a/stack_orchestrator/deploy/webapp/request_webapp_deployment.py b/stack_orchestrator/deploy/webapp/request_webapp_deployment.py index 616db3fd..e846381b 100644 --- a/stack_orchestrator/deploy/webapp/request_webapp_deployment.py +++ b/stack_orchestrator/deploy/webapp/request_webapp_deployment.py @@ -30,10 +30,12 @@ from stack_orchestrator.deploy.webapp.util import ( ) from dotenv import dotenv_values + def fatal(msg: str): print(msg, file=sys.stderr) sys.exit(1) + @click.command() @click.option( "--laconic-config", help="Provide a config file for laconicd", required=True @@ -67,7 +69,7 @@ def fatal(msg: str): is_flag=True, ) @click.pass_context -def command( +def command( # noqa: C901 ctx, laconic_config, app, @@ -79,7 +81,7 @@ def command( use_payment, dns, dry_run, -): # noqa: C901 +): if auction_id and deployer: print("Cannot specify both --auction-id and --deployer", file=sys.stderr) sys.exit(2) @@ -124,9 +126,10 @@ def command( auction_winners = auction.winnerAddresses - # Get deloyer record for all the auction winners + # Get deployer record for all the auction winners for auction_winner in auction_winners: - deployer_records_by_owner = laconic.webapp_deployers({ "--paymentAddress": auction_winner }) + # TODO: Match auction winner address with provider address? + deployer_records_by_owner = laconic.webapp_deployers({"--paymentAddress": auction_winner}) if len(deployer_records_by_owner) == 0: print(f"WARNING: Unable to locate deployer for auction winner {auction_winner}") diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index 5321900e..296c0389 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -28,6 +28,7 @@ TOKEN_DENOM = "alnt" AUCTION_KIND_PROVIDER = "provider" AUCTION_STATUS_COMPLETED = "completed" + class AttrDict(dict): def __init__(self, *args, **kwargs): super(AttrDict, self).__init__(*args, **kwargs) @@ -61,6 +62,12 @@ class TimedLogger: self.last = datetime.datetime.now() +def load_known_requests(filename): + if filename and os.path.exists(filename): + return json.load(open(filename, "r")) + return {} + + def logged_cmd(log_file, *vargs): result = None try: @@ -94,6 +101,7 @@ def is_lrn(name_or_id: str): def is_id(name_or_id: str): return not is_lrn(name_or_id) + class LaconicRegistryClient: def __init__(self, config_file, log_file=None): self.config_file = config_file @@ -321,7 +329,7 @@ class LaconicRegistryClient: results = [ AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r ] - except: + except: # noqa: E722 pass if len(results): @@ -367,6 +375,13 @@ class LaconicRegistryClient: criteria["type"] = "WebappDeployer" return self.list_records(criteria, all) + def app_deployment_auctions(self, criteria=None, all=True): + if criteria is None: + criteria = {} + criteria = criteria.copy() + criteria["type"] = "ApplicationDeploymentAuction" + return self.list_records(criteria, all) + def publish(self, record, names=None): if names is None: names = [] @@ -492,6 +507,7 @@ class LaconicRegistryClient: return json.loads(logged_cmd(self.log_file, *args))["auctionId"] + def file_hash(filename): return hashlib.sha1(open(filename).read().encode()).hexdigest() @@ -751,6 +767,7 @@ def skip_by_tag(r, include_tags, exclude_tags): return False + def confirm_payment(laconic: LaconicRegistryClient, record, payment_address, min_amount, logger): req_owner = laconic.get_owner(record) if req_owner == payment_address: diff --git a/stack_orchestrator/main.py b/stack_orchestrator/main.py index be61f68a..b8ca430d 100644 --- a/stack_orchestrator/main.py +++ b/stack_orchestrator/main.py @@ -27,6 +27,7 @@ from stack_orchestrator.deploy.webapp import (run_webapp, undeploy_webapp_from_registry, publish_webapp_deployer, publish_deployment_auction, + handle_deployment_auction, request_webapp_deployment) from stack_orchestrator.deploy import deploy from stack_orchestrator import version @@ -66,6 +67,7 @@ cli.add_command(deploy_webapp_from_registry.command, "deploy-webapp-from-registr cli.add_command(undeploy_webapp_from_registry.command, "undeploy-webapp-from-registry") cli.add_command(publish_webapp_deployer.command, "publish-deployer-to-registry") cli.add_command(publish_deployment_auction.command, "publish-deployment-auction") +cli.add_command(handle_deployment_auction.command, "handle-deployment-auction") cli.add_command(request_webapp_deployment.command, "request-webapp-deployment") cli.add_command(deploy.command, "deploy") # deploy is an alias for deploy-system cli.add_command(deploy.command, "deploy-system")