diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py index aa3a4d8f..3443b0bd 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, ) @@ -256,13 +257,6 @@ def process_app_deployment_request( logger.log("Publication complete.") 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..a9f44467 --- /dev/null +++ b/stack_orchestrator/deploy/webapp/handle_deployment_auction.py @@ -0,0 +1,159 @@ +# 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) + + # Collapse related requests. + 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/request_webapp_deployment.py b/stack_orchestrator/deploy/webapp/request_webapp_deployment.py index 616db3fd..5d7b8a75 100644 --- a/stack_orchestrator/deploy/webapp/request_webapp_deployment.py +++ b/stack_orchestrator/deploy/webapp/request_webapp_deployment.py @@ -124,8 +124,9 @@ 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: + # 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..557f9ae4 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -60,6 +60,10 @@ class TimedLogger: self.file.flush() 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 @@ -367,6 +371,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 = []