Integrate SP auctions in webapp deployment flow #950
@ -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
|
||||
|
162
stack_orchestrator/deploy/webapp/handle_deployment_auction.py
Normal file
162
stack_orchestrator/deploy/webapp/handle_deployment_auction.py
Normal 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
|
@ -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(
|
||||
|
@ -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}")
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user