WIP: Require payment for app deployment requests.
Some checks failed
Lint Checks / Run linter (pull_request) Failing after 42s
Deploy Test / Run deploy test suite (pull_request) Successful in 5m29s
K8s Deployment Control Test / Run deployment control suite on kind/k8s (pull_request) Successful in 7m30s
K8s Deploy Test / Run deploy test suite on kind/k8s (pull_request) Successful in 9m26s
Smoke Test / Run basic test suite (pull_request) Successful in 4m23s
Webapp Test / Run webapp test suite (pull_request) Successful in 5m10s
Some checks failed
Lint Checks / Run linter (pull_request) Failing after 42s
Deploy Test / Run deploy test suite (pull_request) Successful in 5m29s
K8s Deployment Control Test / Run deployment control suite on kind/k8s (pull_request) Successful in 7m30s
K8s Deploy Test / Run deploy test suite on kind/k8s (pull_request) Successful in 9m26s
Smoke Test / Run basic test suite (pull_request) Successful in 4m23s
Webapp Test / Run webapp test suite (pull_request) Successful in 5m10s
This commit is contained in:
parent
e56da7dcc1
commit
0a9d68f4e8
@ -38,6 +38,7 @@ from stack_orchestrator.deploy.webapp.util import (
|
|||||||
generate_hostname_for_app,
|
generate_hostname_for_app,
|
||||||
match_owner,
|
match_owner,
|
||||||
skip_by_tag,
|
skip_by_tag,
|
||||||
|
confirm_payment,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -78,6 +79,9 @@ def process_app_deployment_request(
|
|||||||
else:
|
else:
|
||||||
fqdn = f"{requested_name}.{default_dns_suffix}"
|
fqdn = f"{requested_name}.{default_dns_suffix}"
|
||||||
|
|
||||||
|
# Normalize case (just in case)
|
||||||
|
fqdn = fqdn.lower()
|
||||||
|
|
||||||
# 3. check ownership of existing dnsrecord vs this request
|
# 3. check ownership of existing dnsrecord vs this request
|
||||||
dns_lrn = f"{dns_record_namespace}/{fqdn}"
|
dns_lrn = f"{dns_record_namespace}/{fqdn}"
|
||||||
dns_record = laconic.get_record(dns_lrn)
|
dns_record = laconic.get_record(dns_lrn)
|
||||||
@ -119,7 +123,7 @@ def process_app_deployment_request(
|
|||||||
app_deployment_lrn = app_deployment_request.attributes.deployment
|
app_deployment_lrn = app_deployment_request.attributes.deployment
|
||||||
if not app_deployment_lrn.startswith(deployment_record_namespace):
|
if not app_deployment_lrn.startswith(deployment_record_namespace):
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Deployment CRN %s is not in a supported namespace"
|
"Deployment LRN %s is not in a supported namespace"
|
||||||
% app_deployment_request.attributes.deployment
|
% app_deployment_request.attributes.deployment
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -305,6 +309,23 @@ def dump_known_requests(filename, requests, status="SEEN"):
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--log-dir", help="Output build/deployment logs to directory.", default=None
|
"--log-dir", help="Output build/deployment logs to directory.", default=None
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--min-required-payment",
|
||||||
|
help="Requests must have a minimum payment to be processed",
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--payment-address",
|
||||||
|
help="The address to which payments should be made. "
|
||||||
|
"Default is the current laconic account.",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--all-requests",
|
||||||
|
help="Handle requests addressed to anyone (by default only requests to"
|
||||||
|
"my payment address are examined).",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def command( # noqa: C901
|
def command( # noqa: C901
|
||||||
ctx,
|
ctx,
|
||||||
@ -326,6 +347,9 @@ def command( # noqa: C901
|
|||||||
force_rebuild,
|
force_rebuild,
|
||||||
recreate_on_deploy,
|
recreate_on_deploy,
|
||||||
log_dir,
|
log_dir,
|
||||||
|
min_required_payment,
|
||||||
|
payment_address,
|
||||||
|
all_requests,
|
||||||
):
|
):
|
||||||
if request_id and discover:
|
if request_id and discover:
|
||||||
print("Cannot specify both --request-id and --discover", file=sys.stderr)
|
print("Cannot specify both --request-id and --discover", file=sys.stderr)
|
||||||
@ -366,6 +390,10 @@ def command( # noqa: C901
|
|||||||
exclude_tags = [tag.strip() for tag in exclude_tags.split(",") if tag]
|
exclude_tags = [tag.strip() for tag in exclude_tags.split(",") if tag]
|
||||||
|
|
||||||
laconic = LaconicRegistryClient(laconic_config, log_file=sys.stderr)
|
laconic = LaconicRegistryClient(laconic_config, log_file=sys.stderr)
|
||||||
|
if not payment_address:
|
||||||
|
payment_address = laconic.whoami().address
|
||||||
|
|
||||||
|
main_logger.log(f"Payment address: {payment_address}")
|
||||||
|
|
||||||
# Find deployment requests.
|
# Find deployment requests.
|
||||||
# single request
|
# single request
|
||||||
@ -375,18 +403,20 @@ def command( # noqa: C901
|
|||||||
# all requests
|
# all requests
|
||||||
elif discover:
|
elif discover:
|
||||||
main_logger.log("Discovering deployment requests...")
|
main_logger.log("Discovering deployment requests...")
|
||||||
requests = laconic.app_deployment_requests()
|
if all_requests:
|
||||||
|
requests = laconic.app_deployment_requests()
|
||||||
|
else:
|
||||||
|
requests = laconic.app_deployment_requests({"to": payment_address})
|
||||||
|
|
||||||
if only_update_state:
|
if only_update_state:
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
dump_known_requests(state_file, requests)
|
dump_known_requests(state_file, requests)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
previous_requests = {}
|
||||||
if state_file:
|
if state_file:
|
||||||
main_logger.log(f"Loading known requests from {state_file}...")
|
main_logger.log(f"Loading known requests from {state_file}...")
|
||||||
previous_requests = load_known_requests(state_file)
|
previous_requests = load_known_requests(state_file)
|
||||||
else:
|
|
||||||
previous_requests = {}
|
|
||||||
|
|
||||||
# Collapse related requests.
|
# Collapse related requests.
|
||||||
requests.sort(key=lambda r: r.createTime)
|
requests.sort(key=lambda r: r.createTime)
|
||||||
@ -466,7 +496,7 @@ def command( # noqa: C901
|
|||||||
if r.attributes.request:
|
if r.attributes.request:
|
||||||
cancellation_requests[r.attributes.request] = r
|
cancellation_requests[r.attributes.request] = r
|
||||||
|
|
||||||
requests_to_execute = []
|
requests_to_check_for_payment = []
|
||||||
for r in requests_by_name.values():
|
for r in requests_by_name.values():
|
||||||
if r.id in cancellation_requests and match_owner(
|
if r.id in cancellation_requests and match_owner(
|
||||||
cancellation_requests[r.id], r
|
cancellation_requests[r.id], r
|
||||||
@ -488,7 +518,24 @@ def command( # noqa: C901
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
main_logger.log(f"Request {r.id} needs to processed.")
|
main_logger.log(f"Request {r.id} needs to processed.")
|
||||||
|
requests_to_check_for_payment.append(r)
|
||||||
|
|
||||||
|
requests_to_execute = []
|
||||||
|
if min_required_payment:
|
||||||
|
for r in requests_to_check_for_payment:
|
||||||
|
main_logger.log(f"{r.id}: Confirming payment...")
|
||||||
|
if confirm_payment(
|
||||||
|
laconic, r, payment_address, min_required_payment, main_logger
|
||||||
|
):
|
||||||
|
main_logger.log(f"{r.id}: Payment confirmed.")
|
||||||
requests_to_execute.append(r)
|
requests_to_execute.append(r)
|
||||||
|
else:
|
||||||
|
main_logger.log(
|
||||||
|
f"Skipping request {r.id}: unable to verify payment."
|
||||||
|
)
|
||||||
|
dump_known_requests(state_file, [r], status="UNPAID")
|
||||||
|
else:
|
||||||
|
requests_to_execute = requests_to_check_for_payment
|
||||||
|
|
||||||
main_logger.log(
|
main_logger.log(
|
||||||
"Found %d unsatisfied request(s) to process." % len(requests_to_execute)
|
"Found %d unsatisfied request(s) to process." % len(requests_to_execute)
|
||||||
|
@ -20,18 +20,31 @@ import sys
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from stack_orchestrator.deploy.webapp.util import LaconicRegistryClient, match_owner, skip_by_tag
|
from stack_orchestrator.deploy.webapp.util import (
|
||||||
|
TimedLogger,
|
||||||
|
LaconicRegistryClient,
|
||||||
|
match_owner,
|
||||||
|
skip_by_tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
main_logger = TimedLogger(file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def process_app_removal_request(ctx,
|
def process_app_removal_request(
|
||||||
laconic: LaconicRegistryClient,
|
ctx,
|
||||||
app_removal_request,
|
laconic: LaconicRegistryClient,
|
||||||
deployment_parent_dir,
|
app_removal_request,
|
||||||
delete_volumes,
|
deployment_parent_dir,
|
||||||
delete_names):
|
delete_volumes,
|
||||||
deployment_record = laconic.get_record(app_removal_request.attributes.deployment, require=True)
|
delete_names,
|
||||||
|
):
|
||||||
|
deployment_record = laconic.get_record(
|
||||||
|
app_removal_request.attributes.deployment, require=True
|
||||||
|
)
|
||||||
dns_record = laconic.get_record(deployment_record.attributes.dns, require=True)
|
dns_record = laconic.get_record(deployment_record.attributes.dns, require=True)
|
||||||
deployment_dir = os.path.join(deployment_parent_dir, dns_record.attributes.name)
|
deployment_dir = os.path.join(
|
||||||
|
deployment_parent_dir, dns_record.attributes.name.lower()
|
||||||
|
)
|
||||||
|
|
||||||
if not os.path.exists(deployment_dir):
|
if not os.path.exists(deployment_dir):
|
||||||
raise Exception("Deployment directory %s does not exist." % deployment_dir)
|
raise Exception("Deployment directory %s does not exist." % deployment_dir)
|
||||||
@ -41,13 +54,18 @@ def process_app_removal_request(ctx,
|
|||||||
|
|
||||||
# Or of the original deployment request.
|
# Or of the original deployment request.
|
||||||
if not matched_owner and deployment_record.attributes.request:
|
if not matched_owner and deployment_record.attributes.request:
|
||||||
matched_owner = match_owner(app_removal_request, laconic.get_record(deployment_record.attributes.request, require=True))
|
matched_owner = match_owner(
|
||||||
|
app_removal_request,
|
||||||
|
laconic.get_record(deployment_record.attributes.request, require=True),
|
||||||
|
)
|
||||||
|
|
||||||
if matched_owner:
|
if matched_owner:
|
||||||
print("Matched deployment ownership:", matched_owner)
|
main_logger.log("Matched deployment ownership:", matched_owner)
|
||||||
else:
|
else:
|
||||||
raise Exception("Unable to confirm ownership of deployment %s for removal request %s" %
|
raise Exception(
|
||||||
(deployment_record.id, app_removal_request.id))
|
"Unable to confirm ownership of deployment %s for removal request %s"
|
||||||
|
% (deployment_record.id, app_removal_request.id)
|
||||||
|
)
|
||||||
|
|
||||||
# TODO(telackey): Call the function directly. The easiest way to build the correct click context is to
|
# TODO(telackey): Call the function directly. The easiest way to build the correct click context is to
|
||||||
# exec the process, but it would be better to refactor so we could just call down_operation with the
|
# exec the process, but it would be better to refactor so we could just call down_operation with the
|
||||||
@ -97,22 +115,65 @@ def dump_known_requests(filename, requests):
|
|||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("--laconic-config", help="Provide a config file for laconicd", required=True)
|
@click.option(
|
||||||
@click.option("--deployment-parent-dir", help="Create deployment directories beneath this directory", required=True)
|
"--laconic-config", help="Provide a config file for laconicd", required=True
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--deployment-parent-dir",
|
||||||
|
help="Create deployment directories beneath this directory",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
@click.option("--request-id", help="The ApplicationDeploymentRemovalRequest to process")
|
@click.option("--request-id", help="The ApplicationDeploymentRemovalRequest to process")
|
||||||
@click.option("--discover", help="Discover and process all pending ApplicationDeploymentRemovalRequests",
|
@click.option(
|
||||||
is_flag=True, default=False)
|
"--discover",
|
||||||
@click.option("--state-file", help="File to store state about previously seen requests.")
|
help="Discover and process all pending ApplicationDeploymentRemovalRequests",
|
||||||
@click.option("--only-update-state", help="Only update the state file, don't process any requests anything.", is_flag=True)
|
is_flag=True,
|
||||||
@click.option("--delete-names/--preserve-names", help="Delete all names associated with removed deployments.", default=True)
|
default=False,
|
||||||
@click.option("--delete-volumes/--preserve-volumes", default=True, help="delete data volumes")
|
)
|
||||||
@click.option("--dry-run", help="Don't do anything, just report what would be done.", is_flag=True)
|
@click.option(
|
||||||
@click.option("--include-tags", help="Only include requests with matching tags (comma-separated).", default="")
|
"--state-file", help="File to store state about previously seen requests."
|
||||||
@click.option("--exclude-tags", help="Exclude requests with matching tags (comma-separated).", default="")
|
)
|
||||||
|
@click.option(
|
||||||
|
"--only-update-state",
|
||||||
|
help="Only update the state file, don't process any requests anything.",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--delete-names/--preserve-names",
|
||||||
|
help="Delete all names associated with removed deployments.",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--delete-volumes/--preserve-volumes", default=True, help="delete data volumes"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dry-run", help="Don't do anything, just report what would be done.", is_flag=True
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--include-tags",
|
||||||
|
help="Only include requests with matching tags (comma-separated).",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--exclude-tags",
|
||||||
|
help="Exclude requests with matching tags (comma-separated).",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def command(ctx, laconic_config, deployment_parent_dir,
|
def command(
|
||||||
request_id, discover, state_file, only_update_state,
|
ctx,
|
||||||
delete_names, delete_volumes, dry_run, include_tags, exclude_tags):
|
laconic_config,
|
||||||
|
deployment_parent_dir,
|
||||||
|
request_id,
|
||||||
|
discover,
|
||||||
|
state_file,
|
||||||
|
only_update_state,
|
||||||
|
delete_names,
|
||||||
|
delete_volumes,
|
||||||
|
dry_run,
|
||||||
|
include_tags,
|
||||||
|
exclude_tags,
|
||||||
|
):
|
||||||
if request_id and discover:
|
if request_id and discover:
|
||||||
print("Cannot specify both --request-id and --discover", file=sys.stderr)
|
print("Cannot specify both --request-id and --discover", file=sys.stderr)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
@ -134,10 +195,12 @@ def command(ctx, laconic_config, deployment_parent_dir,
|
|||||||
# Find deployment removal requests.
|
# Find deployment removal requests.
|
||||||
# single request
|
# single request
|
||||||
if request_id:
|
if request_id:
|
||||||
|
main_logger.log(f"Retrieving request {request_id}...")
|
||||||
requests = [laconic.get_record(request_id, require=True)]
|
requests = [laconic.get_record(request_id, require=True)]
|
||||||
# TODO: assert record type
|
# TODO: assert record type
|
||||||
# all requests
|
# all requests
|
||||||
elif discover:
|
elif discover:
|
||||||
|
main_logger.log("Discovering removal requests...")
|
||||||
requests = laconic.app_deployment_removal_requests()
|
requests = laconic.app_deployment_removal_requests()
|
||||||
|
|
||||||
if only_update_state:
|
if only_update_state:
|
||||||
@ -145,18 +208,24 @@ def command(ctx, laconic_config, deployment_parent_dir,
|
|||||||
dump_known_requests(state_file, requests)
|
dump_known_requests(state_file, requests)
|
||||||
return
|
return
|
||||||
|
|
||||||
previous_requests = load_known_requests(state_file)
|
previous_requests = {}
|
||||||
|
if state_file:
|
||||||
|
main_logger.log(f"Loading known requests from {state_file}...")
|
||||||
|
previous_requests = load_known_requests(state_file)
|
||||||
|
|
||||||
requests.sort(key=lambda r: r.createTime)
|
requests.sort(key=lambda r: r.createTime)
|
||||||
requests.reverse()
|
requests.reverse()
|
||||||
|
|
||||||
# Find deployments.
|
# Find deployments.
|
||||||
deployments = {}
|
named_deployments = {}
|
||||||
for d in laconic.app_deployments(all=True):
|
main_logger.log("Discovering app deployments...")
|
||||||
deployments[d.id] = d
|
for d in laconic.app_deployments(all=False):
|
||||||
|
named_deployments[d.id] = d
|
||||||
|
|
||||||
# Find removal requests.
|
# Find removal requests.
|
||||||
removals_by_deployment = {}
|
removals_by_deployment = {}
|
||||||
removals_by_request = {}
|
removals_by_request = {}
|
||||||
|
main_logger.log("Discovering deployment removals...")
|
||||||
for r in laconic.app_deployment_removals():
|
for r in laconic.app_deployment_removals():
|
||||||
if r.attributes.deployment:
|
if r.attributes.deployment:
|
||||||
# TODO: should we handle CRNs?
|
# TODO: should we handle CRNs?
|
||||||
@ -165,33 +234,50 @@ def command(ctx, laconic_config, deployment_parent_dir,
|
|||||||
one_per_deployment = {}
|
one_per_deployment = {}
|
||||||
for r in requests:
|
for r in requests:
|
||||||
if not r.attributes.deployment:
|
if not r.attributes.deployment:
|
||||||
print(f"Skipping removal request {r.id} since it was a cancellation.")
|
main_logger.log(
|
||||||
|
f"Skipping removal request {r.id} since it was a cancellation."
|
||||||
|
)
|
||||||
elif r.attributes.deployment in one_per_deployment:
|
elif r.attributes.deployment in one_per_deployment:
|
||||||
print(f"Skipping removal request {r.id} since it was superseded.")
|
main_logger.log(f"Skipping removal request {r.id} since it was superseded.")
|
||||||
else:
|
else:
|
||||||
one_per_deployment[r.attributes.deployment] = r
|
one_per_deployment[r.attributes.deployment] = r
|
||||||
|
|
||||||
requests_to_execute = []
|
requests_to_execute = []
|
||||||
for r in one_per_deployment.values():
|
for r in one_per_deployment.values():
|
||||||
if skip_by_tag(r, include_tags, exclude_tags):
|
try:
|
||||||
print("Skipping removal request %s, filtered by tag (include %s, exclude %s, present %s)" % (r.id,
|
if r.attributes.deployment not in named_deployments:
|
||||||
include_tags,
|
main_logger.log(
|
||||||
exclude_tags,
|
f"Skipping removal request {r.id} for {r.attributes.deployment} because it does"
|
||||||
r.attributes.tags))
|
f"not appear to refer to a live, named deployment."
|
||||||
elif r.id in removals_by_request:
|
)
|
||||||
print(f"Found satisfied request for {r.id} at {removals_by_request[r.id].id}")
|
elif skip_by_tag(r, include_tags, exclude_tags):
|
||||||
elif r.attributes.deployment in removals_by_deployment:
|
main_logger.log(
|
||||||
print(
|
"Skipping removal request %s, filtered by tag (include %s, exclude %s, present %s)"
|
||||||
f"Found removal record for indicated deployment {r.attributes.deployment} at "
|
% (r.id, include_tags, exclude_tags, r.attributes.tags)
|
||||||
f"{removals_by_deployment[r.attributes.deployment].id}")
|
)
|
||||||
else:
|
elif r.id in removals_by_request:
|
||||||
if r.id not in previous_requests:
|
main_logger.log(
|
||||||
print(f"Request {r.id} needs to processed.")
|
f"Found satisfied request for {r.id} at {removals_by_request[r.id].id}"
|
||||||
requests_to_execute.append(r)
|
)
|
||||||
|
elif r.attributes.deployment in removals_by_deployment:
|
||||||
|
main_logger.log(
|
||||||
|
f"Found removal record for indicated deployment {r.attributes.deployment} at "
|
||||||
|
f"{removals_by_deployment[r.attributes.deployment].id}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print(f"Skipping unsatisfied request {r.id} because we have seen it before.")
|
if r.id not in previous_requests:
|
||||||
|
main_logger.log(f"Request {r.id} needs to processed.")
|
||||||
|
requests_to_execute.append(r)
|
||||||
|
else:
|
||||||
|
main_logger.log(
|
||||||
|
f"Skipping unsatisfied request {r.id} because we have seen it before."
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
main_logger.log(f"ERROR examining {r.id}: {e}")
|
||||||
|
|
||||||
print("Found %d unsatisfied request(s) to process." % len(requests_to_execute))
|
main_logger.log(
|
||||||
|
"Found %d unsatisfied request(s) to process." % len(requests_to_execute)
|
||||||
|
)
|
||||||
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
for r in requests_to_execute:
|
for r in requests_to_execute:
|
||||||
@ -202,7 +288,9 @@ def command(ctx, laconic_config, deployment_parent_dir,
|
|||||||
r,
|
r,
|
||||||
os.path.abspath(deployment_parent_dir),
|
os.path.abspath(deployment_parent_dir),
|
||||||
delete_volumes,
|
delete_volumes,
|
||||||
delete_names
|
delete_names,
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
main_logger.log(f"ERROR processing removal request {r.id}: {e}")
|
||||||
finally:
|
finally:
|
||||||
dump_known_requests(state_file, [r])
|
dump_known_requests(state_file, [r])
|
||||||
|
@ -22,6 +22,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
|
from typing import final
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
@ -83,6 +84,41 @@ def match_owner(recordA, *records):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_lrn(name_or_id: str):
|
||||||
|
if name_or_id:
|
||||||
|
return str(name_or_id).startswith("lrn://")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_id(name_or_id: str):
|
||||||
|
return not is_lrn(name_or_id)
|
||||||
|
|
||||||
|
def confirm_payment(laconic, record, payment_address, min_amount, logger):
|
||||||
|
if not record.attributes.payment:
|
||||||
|
logger.log(f"{record.id}: not payment tx")
|
||||||
|
return False
|
||||||
|
|
||||||
|
tx = laconic.get_tx(record.attributes.payment)
|
||||||
|
if not tx:
|
||||||
|
logger.log(f"{record.id}: cannot locate payment tx")
|
||||||
|
return False
|
||||||
|
|
||||||
|
owner = laconic.get_owner(record)
|
||||||
|
if tx.from_address != owner:
|
||||||
|
logger.log(f"{record.id}: {tx.from_address} != {owner}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if tx.to_address != payment_address:
|
||||||
|
logger.log(f"{record.id}: {tx.to_address} != {payment_address}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if tx.amount < min_amount:
|
||||||
|
logger.log(f"{record.id}: {tx.amount} < {min_amount}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class LaconicRegistryClient:
|
class LaconicRegistryClient:
|
||||||
def __init__(self, config_file, log_file=None):
|
def __init__(self, config_file, log_file=None):
|
||||||
self.config_file = config_file
|
self.config_file = config_file
|
||||||
@ -90,10 +126,64 @@ class LaconicRegistryClient:
|
|||||||
self.cache = AttrDict(
|
self.cache = AttrDict(
|
||||||
{
|
{
|
||||||
"name_or_id": {},
|
"name_or_id": {},
|
||||||
|
"accounts": {}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def list_records(self, criteria={}, all=False):
|
def whoami(self, refresh=False):
|
||||||
|
if not refresh and "whoami" in self.cache:
|
||||||
|
return self.cache["whoami"]
|
||||||
|
|
||||||
|
args = ["laconic", "-c", self.config_file, "registry", "account", "get"]
|
||||||
|
results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r]
|
||||||
|
|
||||||
|
if len(results):
|
||||||
|
self.cache["whoami"] = results[0]
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_owner(self, record):
|
||||||
|
bond = self.get_bond(record.bondId, require=True)
|
||||||
|
return bond.owner
|
||||||
|
|
||||||
|
def get_account(self, address, refresh=False, require=False):
|
||||||
|
if not refresh and address in self.cache["accounts"]:
|
||||||
|
return self.cache["accounts"][address]
|
||||||
|
|
||||||
|
args = ["laconic", "-c", self.config_file, "registry", "account", "get", "--address", address]
|
||||||
|
results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r]
|
||||||
|
if len(results):
|
||||||
|
self.cache["accounts"][address] = results[0]
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
if require:
|
||||||
|
raise Exception("Cannot locate account:", address)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_bond(self, id, require=False):
|
||||||
|
if id in self.cache.name_or_id:
|
||||||
|
return self.cache.name_or_id[id]
|
||||||
|
|
||||||
|
args = ["laconic", "-c", self.config_file, "registry", "bond", "get", "--id", id]
|
||||||
|
results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r]
|
||||||
|
self._add_to_cache(results)
|
||||||
|
if len(results):
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
if require:
|
||||||
|
raise Exception("Cannot locate bond:", id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_bonds(self):
|
||||||
|
args = ["laconic", "-c", self.config_file, "registry", "bond", "list"]
|
||||||
|
results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r]
|
||||||
|
self._add_to_cache(results)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def list_records(self, criteria=None, all=False):
|
||||||
|
if criteria is None:
|
||||||
|
criteria = {}
|
||||||
args = ["laconic", "-c", self.config_file, "registry", "record", "list"]
|
args = ["laconic", "-c", self.config_file, "registry", "record", "list"]
|
||||||
|
|
||||||
if all:
|
if all:
|
||||||
@ -104,22 +194,15 @@ class LaconicRegistryClient:
|
|||||||
args.append("--%s" % k)
|
args.append("--%s" % k)
|
||||||
args.append(str(v))
|
args.append(str(v))
|
||||||
|
|
||||||
results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args))]
|
results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r]
|
||||||
|
|
||||||
# Most recent records first
|
# Most recent records first
|
||||||
results.sort(key=lambda r: r.createTime)
|
results.sort(key=lambda r: r.createTime)
|
||||||
results.reverse()
|
results.reverse()
|
||||||
|
self._add_to_cache(results)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def is_lrn(self, name_or_id: str):
|
|
||||||
if name_or_id:
|
|
||||||
return str(name_or_id).startswith("lrn://")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_id(self, name_or_id: str):
|
|
||||||
return not self.is_lrn(name_or_id)
|
|
||||||
|
|
||||||
def _add_to_cache(self, records):
|
def _add_to_cache(self, records):
|
||||||
if not records:
|
if not records:
|
||||||
return
|
return
|
||||||
@ -129,9 +212,10 @@ class LaconicRegistryClient:
|
|||||||
if p.names:
|
if p.names:
|
||||||
for lrn in p.names:
|
for lrn in p.names:
|
||||||
self.cache["name_or_id"][lrn] = p
|
self.cache["name_or_id"][lrn] = p
|
||||||
if p.attributes.type not in self.cache:
|
if p.attributes and p.attributes.type:
|
||||||
self.cache[p.attributes.type] = []
|
if p.attributes.type not in self.cache:
|
||||||
self.cache[p.attributes.type].append(p)
|
self.cache[p.attributes.type] = []
|
||||||
|
self.cache[p.attributes.type].append(p)
|
||||||
|
|
||||||
def resolve(self, name):
|
def resolve(self, name):
|
||||||
if not name:
|
if not name:
|
||||||
@ -142,7 +226,7 @@ class LaconicRegistryClient:
|
|||||||
|
|
||||||
args = ["laconic", "-c", self.config_file, "registry", "name", "resolve", name]
|
args = ["laconic", "-c", self.config_file, "registry", "name", "resolve", name]
|
||||||
|
|
||||||
parsed = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args))]
|
parsed = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r]
|
||||||
if parsed:
|
if parsed:
|
||||||
self._add_to_cache(parsed)
|
self._add_to_cache(parsed)
|
||||||
return parsed[0]
|
return parsed[0]
|
||||||
@ -158,7 +242,7 @@ class LaconicRegistryClient:
|
|||||||
if name_or_id in self.cache.name_or_id:
|
if name_or_id in self.cache.name_or_id:
|
||||||
return self.cache.name_or_id[name_or_id]
|
return self.cache.name_or_id[name_or_id]
|
||||||
|
|
||||||
if self.is_lrn(name_or_id):
|
if is_lrn(name_or_id):
|
||||||
return self.resolve(name_or_id)
|
return self.resolve(name_or_id)
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
@ -181,19 +265,37 @@ class LaconicRegistryClient:
|
|||||||
raise Exception("Cannot locate record:", name_or_id)
|
raise Exception("Cannot locate record:", name_or_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def app_deployment_requests(self, all=True):
|
def app_deployment_requests(self, criteria=None, all=True):
|
||||||
return self.list_records({"type": "ApplicationDeploymentRequest"}, all)
|
if criteria is None:
|
||||||
|
criteria = {}
|
||||||
|
criteria = criteria.copy()
|
||||||
|
criteria["type"] = "ApplicationDeploymentRequest"
|
||||||
|
return self.list_records(criteria, all)
|
||||||
|
|
||||||
def app_deployments(self, all=True):
|
def app_deployments(self, criteria=None, all=True):
|
||||||
return self.list_records({"type": "ApplicationDeploymentRecord"}, all)
|
if criteria is None:
|
||||||
|
criteria = {}
|
||||||
|
criteria = criteria.copy()
|
||||||
|
criteria["type"] = "ApplicationDeploymentRecord"
|
||||||
|
return self.list_records(criteria, all)
|
||||||
|
|
||||||
def app_deployment_removal_requests(self, all=True):
|
def app_deployment_removal_requests(self, criteria=None, all=True):
|
||||||
return self.list_records({"type": "ApplicationDeploymentRemovalRequest"}, all)
|
if criteria is None:
|
||||||
|
criteria = {}
|
||||||
|
criteria = criteria.copy()
|
||||||
|
criteria["type"] = "ApplicationDeploymentRemovalRequest"
|
||||||
|
return self.list_records(criteria, all)
|
||||||
|
|
||||||
def app_deployment_removals(self, all=True):
|
def app_deployment_removals(self, criteria=None, all=True):
|
||||||
return self.list_records({"type": "ApplicationDeploymentRemovalRecord"}, all)
|
if criteria is None:
|
||||||
|
criteria = {}
|
||||||
|
criteria = criteria.copy()
|
||||||
|
criteria["type"] = "ApplicationDeploymentRemovalRecord"
|
||||||
|
return self.list_records(criteria, all)
|
||||||
|
|
||||||
def publish(self, record, names=[]):
|
def publish(self, record, names=None):
|
||||||
|
if names is None:
|
||||||
|
names = []
|
||||||
tmpdir = tempfile.mkdtemp()
|
tmpdir = tempfile.mkdtemp()
|
||||||
try:
|
try:
|
||||||
record_fname = os.path.join(tmpdir, "record.yml")
|
record_fname = os.path.join(tmpdir, "record.yml")
|
||||||
@ -248,7 +350,9 @@ def determine_base_container(clone_dir, app_type="webapp"):
|
|||||||
return base_container
|
return base_container
|
||||||
|
|
||||||
|
|
||||||
def build_container_image(app_record, tag, extra_build_args=[], logger=None):
|
def build_container_image(app_record, tag, extra_build_args=None, logger=None):
|
||||||
|
if extra_build_args is None:
|
||||||
|
extra_build_args = []
|
||||||
tmpdir = tempfile.mkdtemp()
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
|
||||||
# TODO: determine if this code could be calling into the Python git library like setup-repositories
|
# TODO: determine if this code could be calling into the Python git library like setup-repositories
|
||||||
|
Loading…
Reference in New Issue
Block a user