Update request-webapp-deployment command to handle deployment auction
This commit is contained in:
parent
8051a3fc6b
commit
61e1116e33
@ -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")
|
||||||
|
@ -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)
|
||||||
|
@ -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 = []
|
||||||
|
Loading…
Reference in New Issue
Block a user