Add a command to handle deployment auctions
All checks were successful
Lint Checks / Run linter (pull_request) Successful in 32s
Deploy Test / Run deploy test suite (pull_request) Successful in 4m53s
K8s Deployment Control Test / Run deployment control suite on kind/k8s (pull_request) Successful in 6m27s
K8s Deploy Test / Run deploy test suite on kind/k8s (pull_request) Successful in 7m55s
Webapp Test / Run webapp test suite (pull_request) Successful in 4m59s
Smoke Test / Run basic test suite (pull_request) Successful in 3m47s

This commit is contained in:
Prathamesh Musale 2024-10-01 14:47:11 +05:30
parent 02fac0feb7
commit d79834a182
6 changed files with 192 additions and 11 deletions

View File

@ -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

View File

@ -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 <http:#www.gnu.org/licenses/>.
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

View File

@ -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(

View File

@ -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}")

View File

@ -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:

View File

@ -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")