Update request-webapp-deployment command to handle deployment auction

This commit is contained in:
Prathamesh Musale 2024-09-27 15:53:47 +05:30
parent 8051a3fc6b
commit 61e1116e33
3 changed files with 166 additions and 77 deletions

View File

@ -107,6 +107,7 @@ def command(
"num_providers": num_providers, "num_providers": num_providers,
} }
auction_id = laconic.create_auction(provider_auction_params) auction_id = laconic.create_auction(provider_auction_params)
print("Deployment auction created:", auction_id)
if not auction_id: if not auction_id:
fatal("Unable to create a provider auction") fatal("Unable to create a provider auction")

View File

@ -24,16 +24,16 @@ import requests
import yaml import yaml
from stack_orchestrator.deploy.webapp.util import ( from stack_orchestrator.deploy.webapp.util import (
AUCTION_STATUS_COMPLETED,
AUCTION_KIND_PROVIDER,
LaconicRegistryClient, LaconicRegistryClient,
) )
from dotenv import dotenv_values from dotenv import dotenv_values
def fatal(msg: str): def fatal(msg: str):
print(msg, file=sys.stderr) print(msg, file=sys.stderr)
sys.exit(1) sys.exit(1)
@click.command() @click.command()
@click.option( @click.option(
"--laconic-config", help="Provide a config file for laconicd", required=True "--laconic-config", help="Provide a config file for laconicd", required=True
@ -43,10 +43,13 @@ def fatal(msg: str):
help="The LRN of the application to deploy.", help="The LRN of the application to deploy.",
required=True, required=True,
) )
@click.option(
"--auction-id",
help="Deployment auction id. Can be used instead of deployer and payment.",
)
@click.option( @click.option(
"--deployer", "--deployer",
help="The LRN of the deployer to process this request.", help="The LRN of the deployer to process this request.",
required=True,
) )
@click.option("--env-file", help="environment file for webapp") @click.option("--env-file", help="environment file for webapp")
@click.option("--config-ref", help="The ref of an existing config upload to use.") @click.option("--config-ref", help="The ref of an existing config upload to use.")
@ -68,6 +71,7 @@ def command(
ctx, ctx,
laconic_config, laconic_config,
app, app,
auction_id,
deployer, deployer,
env_file, env_file,
config_ref, config_ref,
@ -76,6 +80,17 @@ def command(
dns, dns,
dry_run, dry_run,
): # noqa: C901 ): # noqa: C901
if auction_id and deployer:
print("Cannot specify both --auction-id and --deployer", file=sys.stderr)
sys.exit(2)
if auction_id and (make_payment or use_payment):
print("Cannot specify --auction-id with --make-payment or --use-payment", file=sys.stderr)
sys.exit(2)
if env_file and config_ref:
fatal("Cannot use --env-file and --config-ref at the same time.")
tempdir = tempfile.mkdtemp() tempdir = tempfile.mkdtemp()
try: try:
laconic = LaconicRegistryClient(laconic_config) laconic = LaconicRegistryClient(laconic_config)
@ -84,92 +99,126 @@ def command(
if not app_record: if not app_record:
fatal(f"Unable to locate app: {app}") fatal(f"Unable to locate app: {app}")
deployer_record = laconic.get_record(deployer) # Deployers to send requests to
if not deployer_record: deployer_records = []
fatal(f"Unable to locate deployer: {deployer}")
if env_file and config_ref: auction = None
fatal("Cannot use --env-file and --config-ref at the same time.") auction_winners = None
if auction_id:
# Fetch auction details
auction = laconic.get_auction(auction_id)
if not auction:
fatal(f"Unable to locate auction: {auction_id}")
# If env_file # Check auction kind
if env_file: if auction.kind != AUCTION_KIND_PROVIDER:
gpg = gnupg.GPG(gnupghome=tempdir) fatal(f"Auction kind needs to be ${AUCTION_KIND_PROVIDER}, got {auction.kind}")
# Import the deployer's public key # Check auction status
result = gpg.import_keys( if auction.status != AUCTION_STATUS_COMPLETED:
base64.b64decode(deployer_record.attributes.publicKey) fatal(f"Auction {auction_id} not completed yet, status {auction.status}")
)
if 1 != result.imported:
fatal("Failed to import deployer's public key.")
recip = gpg.list_keys()[0]["uids"][0] # Check that winner list is not empty
if len(auction.winnerAddresses) == 0:
fatal(f"Auction {auction_id} has no winners")
# Wrap the config auction_winners = auction.winnerAddresses
config = {
# Include account (and payment?) details
"authorized": [laconic.whoami().address],
"config": {"env": dict(dotenv_values(env_file))},
}
serialized = yaml.dump(config)
# Encrypt # Get deloyer record for all the auction winners
result = gpg.encrypt(serialized, recip, always_trust=True, armor=False) for auction_winner in auction_winners:
if not result.ok: deployer_records_by_owner = laconic.webapp_deployers({ "owner": auction_winner })
fatal("Failed to encrypt config.") if len(deployer_records_by_owner) == 0:
print(f"WARNING: Unable to locate deployer for auction winner {auction_winner}")
# Upload it to the deployer's API deployer_records.append(deployer_records_by_owner[0])
response = requests.post( else:
f"{deployer_record.attributes.apiUrl}/upload/config", deployer_record = laconic.get_record(deployer)
data=result.data, if not deployer_record:
headers={"Content-Type": "application/octet-stream"}, fatal(f"Unable to locate deployer: {deployer}")
)
if not response.ok:
response.raise_for_status()
config_ref = response.json()["id"] deployer_records.append(deployer_records_by_owner[0])
deployment_request = { # Create and send request to each deployer
"record": { for deployer_record in deployer_records:
"type": "ApplicationDeploymentRequest", # If env_file
"application": app, if env_file:
"version": "1.0.0", gpg = gnupg.GPG(gnupghome=tempdir)
"name": f"{app_record.attributes.name}@{app_record.attributes.version}",
"deployer": deployer,
"meta": {"when": str(datetime.utcnow())},
}
}
if config_ref: # Import the deployer's public key
deployment_request["record"]["config"] = {"ref": config_ref} result = gpg.import_keys(
base64.b64decode(deployer_record.attributes.publicKey)
if dns:
deployment_request["record"]["dns"] = dns.lower()
if make_payment:
amount = 0
if dry_run:
deployment_request["record"]["payment"] = "DRY_RUN"
elif "auto" == make_payment:
if "minimumPayment" in deployer_record.attributes:
amount = int(
deployer_record.attributes.minimumPayment.replace("alnt", "")
)
else:
amount = make_payment
if amount:
receipt = laconic.send_tokens(
deployer_record.attributes.paymentAddress, amount
) )
deployment_request["record"]["payment"] = receipt.tx.hash if 1 != result.imported:
print("Payment TX:", receipt.tx.hash) fatal("Failed to import deployer's public key.")
elif use_payment:
deployment_request["record"]["payment"] = use_payment
if dry_run: recip = gpg.list_keys()[0]["uids"][0]
print(yaml.dump(deployment_request))
return
# Send the request # Wrap the config
laconic.publish(deployment_request) config = {
# Include account (and payment?) details
"authorized": [laconic.whoami().address],
"config": {"env": dict(dotenv_values(env_file))},
}
serialized = yaml.dump(config)
# Encrypt
result = gpg.encrypt(serialized, recip, always_trust=True, armor=False)
if not result.ok:
fatal("Failed to encrypt config.")
# Upload it to the deployer's API
response = requests.post(
f"{deployer_record.attributes.apiUrl}/upload/config",
data=result.data,
headers={"Content-Type": "application/octet-stream"},
)
if not response.ok:
response.raise_for_status()
config_ref = response.json()["id"]
deployment_request = {
"record": {
"type": "ApplicationDeploymentRequest",
"application": app,
"version": "1.0.0",
"name": f"{app_record.attributes.name}@{app_record.attributes.version}",
"deployer": deployer,
"meta": {"when": str(datetime.utcnow())},
}
}
if config_ref:
deployment_request["record"]["config"] = {"ref": config_ref}
if dns:
deployment_request["record"]["dns"] = dns.lower()
if make_payment:
amount = 0
if dry_run:
deployment_request["record"]["payment"] = "DRY_RUN"
elif "auto" == make_payment:
if "minimumPayment" in deployer_record.attributes:
amount = int(
deployer_record.attributes.minimumPayment.replace("alnt", "")
)
else:
amount = make_payment
if amount:
receipt = laconic.send_tokens(
deployer_record.attributes.paymentAddress, amount
)
deployment_request["record"]["payment"] = receipt.tx.hash
print("Payment TX:", receipt.tx.hash)
elif use_payment:
deployment_request["record"]["payment"] = use_payment
if dry_run:
print(yaml.dump(deployment_request))
return
# Send the request
laconic.publish(deployment_request)
finally: finally:
shutil.rmtree(tempdir, ignore_errors=True) shutil.rmtree(tempdir, ignore_errors=True)

View File

@ -24,6 +24,10 @@ import tempfile
import uuid import uuid
import yaml import yaml
TOKEN_DENOM = "alnt"
AUCTION_KIND_PROVIDER = "provider"
AUCTION_STATUS_COMPLETED = "completed"
class AttrDict(dict): class AttrDict(dict):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(AttrDict, self).__init__(*args, **kwargs) super(AttrDict, self).__init__(*args, **kwargs)
@ -300,6 +304,34 @@ class LaconicRegistryClient:
if require: if require:
raise Exception("Cannot locate tx:", hash) raise Exception("Cannot locate tx:", hash)
def get_auction(self, auction_id, require=False):
args = [
"laconic",
"-c",
self.config_file,
"registry",
"auction",
"get",
"--id",
auction_id,
]
results = None
try:
results = [
AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r
]
except:
pass
if len(results):
return results[0]
if require:
raise Exception("Cannot locate auction:", auction_id)
return None
def app_deployment_requests(self, criteria=None, all=True): def app_deployment_requests(self, criteria=None, all=True):
if criteria is None: if criteria is None:
criteria = {} criteria = {}
@ -328,6 +360,13 @@ class LaconicRegistryClient:
criteria["type"] = "ApplicationDeploymentRemovalRecord" criteria["type"] = "ApplicationDeploymentRemovalRecord"
return self.list_records(criteria, all) return self.list_records(criteria, all)
def webapp_deployers(self, criteria=None, all=True):
if criteria is None:
criteria = {}
criteria = criteria.copy()
criteria["type"] = "WebappDeployer"
return self.list_records(criteria, all)
def publish(self, record, names=None): def publish(self, record, names=None):
if names is None: if names is None:
names = [] names = []