diff --git a/stack_orchestrator/deploy/webapp/publish_deployment_auction.py b/stack_orchestrator/deploy/webapp/publish_deployment_auction.py index df96cb0f..edbaa8a8 100644 --- a/stack_orchestrator/deploy/webapp/publish_deployment_auction.py +++ b/stack_orchestrator/deploy/webapp/publish_deployment_auction.py @@ -107,6 +107,7 @@ def command( "num_providers": num_providers, } auction_id = laconic.create_auction(provider_auction_params) + print("Deployment auction created:", auction_id) if not auction_id: fatal("Unable to create a provider auction") diff --git a/stack_orchestrator/deploy/webapp/request_webapp_deployment.py b/stack_orchestrator/deploy/webapp/request_webapp_deployment.py index 466c4c54..4345320d 100644 --- a/stack_orchestrator/deploy/webapp/request_webapp_deployment.py +++ b/stack_orchestrator/deploy/webapp/request_webapp_deployment.py @@ -24,16 +24,16 @@ import requests import yaml from stack_orchestrator.deploy.webapp.util import ( + AUCTION_STATUS_COMPLETED, + AUCTION_KIND_PROVIDER, LaconicRegistryClient, ) 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 @@ -43,10 +43,13 @@ def fatal(msg: str): help="The LRN of the application to deploy.", required=True, ) +@click.option( + "--auction-id", + help="Deployment auction id. Can be used instead of deployer and payment.", +) @click.option( "--deployer", help="The LRN of the deployer to process this request.", - required=True, ) @click.option("--env-file", help="environment file for webapp") @click.option("--config-ref", help="The ref of an existing config upload to use.") @@ -68,6 +71,7 @@ def command( ctx, laconic_config, app, + auction_id, deployer, env_file, config_ref, @@ -76,6 +80,17 @@ def command( dns, dry_run, ): # 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() try: laconic = LaconicRegistryClient(laconic_config) @@ -84,92 +99,126 @@ def command( if not app_record: fatal(f"Unable to locate app: {app}") - deployer_record = laconic.get_record(deployer) - if not deployer_record: - fatal(f"Unable to locate deployer: {deployer}") + # Deployers to send requests to + deployer_records = [] - if env_file and config_ref: - fatal("Cannot use --env-file and --config-ref at the same time.") + auction = None + 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 - if env_file: - gpg = gnupg.GPG(gnupghome=tempdir) + # Check auction kind + if auction.kind != AUCTION_KIND_PROVIDER: + fatal(f"Auction kind needs to be ${AUCTION_KIND_PROVIDER}, got {auction.kind}") - # Import the deployer's public key - result = gpg.import_keys( - base64.b64decode(deployer_record.attributes.publicKey) - ) - if 1 != result.imported: - fatal("Failed to import deployer's public key.") + # Check auction status + if auction.status != AUCTION_STATUS_COMPLETED: + fatal(f"Auction {auction_id} not completed yet, status {auction.status}") - 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 - config = { - # Include account (and payment?) details - "authorized": [laconic.whoami().address], - "config": {"env": dict(dotenv_values(env_file))}, - } - serialized = yaml.dump(config) + auction_winners = auction.winnerAddresses - # Encrypt - result = gpg.encrypt(serialized, recip, always_trust=True, armor=False) - if not result.ok: - fatal("Failed to encrypt config.") + # Get deloyer record for all the auction winners + for auction_winner in auction_winners: + deployer_records_by_owner = laconic.webapp_deployers({ "owner": auction_winner }) + 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 - 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() + deployer_records.append(deployer_records_by_owner[0]) + else: + deployer_record = laconic.get_record(deployer) + if not deployer_record: + fatal(f"Unable to locate deployer: {deployer}") - config_ref = response.json()["id"] + deployer_records.append(deployer_records_by_owner[0]) - 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())}, - } - } + # Create and send request to each deployer + for deployer_record in deployer_records: + # If env_file + if env_file: + gpg = gnupg.GPG(gnupghome=tempdir) - 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 + # Import the deployer's public key + result = gpg.import_keys( + base64.b64decode(deployer_record.attributes.publicKey) ) - deployment_request["record"]["payment"] = receipt.tx.hash - print("Payment TX:", receipt.tx.hash) - elif use_payment: - deployment_request["record"]["payment"] = use_payment + if 1 != result.imported: + fatal("Failed to import deployer's public key.") - if dry_run: - print(yaml.dump(deployment_request)) - return + recip = gpg.list_keys()[0]["uids"][0] - # Send the request - laconic.publish(deployment_request) + # Wrap the config + 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: shutil.rmtree(tempdir, ignore_errors=True) diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index 5f0a903f..5321900e 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -24,6 +24,10 @@ import tempfile import uuid import yaml +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) @@ -300,6 +304,34 @@ class LaconicRegistryClient: if require: 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): if criteria is None: criteria = {} @@ -328,6 +360,13 @@ class LaconicRegistryClient: criteria["type"] = "ApplicationDeploymentRemovalRecord" 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): if names is None: names = []