Archived
Fix pyright type errors across codebase
- Add pyrightconfig.json for pyright 1.1.408 TOML parsing workaround - Add NoReturn annotations to fatal() functions for proper type narrowing - Add None checks and assertions after require=True get_record() calls - Fix AttrDict class with __getattr__ for dynamic attribute access - Add type annotations and casts for Kubernetes client objects - Store compose config as DockerDeployer instance attributes - Filter None values from dotenv and environment mappings - Use hasattr/getattr patterns for optional container attributes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
cd3d908d0d
commit
dd856af2d3
@@ -73,6 +73,7 @@ def process_app_deployment_request(
|
||||
app = laconic.get_record(
|
||||
app_deployment_request.attributes.application, require=True
|
||||
)
|
||||
assert app is not None # require=True ensures this
|
||||
logger.log(f"Retrieved app record {app_deployment_request.attributes.application}")
|
||||
|
||||
# 2. determine dns
|
||||
@@ -483,6 +484,8 @@ def command( # noqa: C901
|
||||
laconic_config, log_file=sys.stderr, mutex_lock_file=registry_lock_file
|
||||
)
|
||||
webapp_deployer_record = laconic.get_record(lrn, require=True)
|
||||
assert webapp_deployer_record is not None # require=True ensures this
|
||||
assert webapp_deployer_record.attributes is not None
|
||||
payment_address = webapp_deployer_record.attributes.paymentAddress
|
||||
main_logger.log(f"Payment address: {payment_address}")
|
||||
|
||||
@@ -495,6 +498,7 @@ def command( # noqa: C901
|
||||
sys.exit(2)
|
||||
|
||||
# Find deployment requests.
|
||||
requests = []
|
||||
# single request
|
||||
if request_id:
|
||||
main_logger.log(f"Retrieving request {request_id}...")
|
||||
@@ -518,25 +522,35 @@ def command( # noqa: C901
|
||||
previous_requests = load_known_requests(state_file)
|
||||
|
||||
# Collapse related requests.
|
||||
requests.sort(key=lambda r: r.createTime)
|
||||
requests.reverse()
|
||||
# Filter out None values and sort
|
||||
valid_requests = [r for r in requests if r is not None]
|
||||
valid_requests.sort(key=lambda r: r.createTime if r else "")
|
||||
valid_requests.reverse()
|
||||
requests_by_name = {}
|
||||
skipped_by_name = {}
|
||||
for r in requests:
|
||||
main_logger.log(f"BEGIN: Examining request {r.id}")
|
||||
for r in valid_requests:
|
||||
if not r:
|
||||
continue
|
||||
r_id = r.id if r else "unknown"
|
||||
main_logger.log(f"BEGIN: Examining request {r_id}")
|
||||
result = "PENDING"
|
||||
try:
|
||||
if (
|
||||
r.id in previous_requests
|
||||
and previous_requests[r.id].get("status", "") != "RETRY"
|
||||
r_id in previous_requests
|
||||
and previous_requests[r_id].get("status", "") != "RETRY"
|
||||
):
|
||||
main_logger.log(f"Skipping request {r.id}, we've already seen it.")
|
||||
main_logger.log(f"Skipping request {r_id}, we've already seen it.")
|
||||
result = "SKIP"
|
||||
continue
|
||||
|
||||
if not r.attributes:
|
||||
main_logger.log(f"Skipping request {r_id}, no attributes.")
|
||||
result = "ERROR"
|
||||
continue
|
||||
|
||||
app = laconic.get_record(r.attributes.application)
|
||||
if not app:
|
||||
main_logger.log(f"Skipping request {r.id}, cannot locate app.")
|
||||
main_logger.log(f"Skipping request {r_id}, cannot locate app.")
|
||||
result = "ERROR"
|
||||
continue
|
||||
|
||||
@@ -544,7 +558,7 @@ def command( # noqa: C901
|
||||
if not requested_name:
|
||||
requested_name = generate_hostname_for_app(app)
|
||||
main_logger.log(
|
||||
"Generating name %s for request %s." % (requested_name, r.id)
|
||||
"Generating name %s for request %s." % (requested_name, r_id)
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -552,31 +566,33 @@ def command( # noqa: C901
|
||||
or requested_name in requests_by_name
|
||||
):
|
||||
main_logger.log(
|
||||
"Ignoring request %s, it has been superseded." % r.id
|
||||
"Ignoring request %s, it has been superseded." % r_id
|
||||
)
|
||||
result = "SKIP"
|
||||
continue
|
||||
|
||||
if skip_by_tag(r, include_tags, exclude_tags):
|
||||
r_tags = r.attributes.tags if r.attributes else None
|
||||
main_logger.log(
|
||||
"Skipping request %s, filtered by tag "
|
||||
"(include %s, exclude %s, present %s)"
|
||||
% (r.id, include_tags, exclude_tags, r.attributes.tags)
|
||||
% (r_id, include_tags, exclude_tags, r_tags)
|
||||
)
|
||||
skipped_by_name[requested_name] = r
|
||||
result = "SKIP"
|
||||
continue
|
||||
|
||||
r_app = r.attributes.application if r.attributes else "unknown"
|
||||
main_logger.log(
|
||||
"Found pending request %s to run application %s on %s."
|
||||
% (r.id, r.attributes.application, requested_name)
|
||||
% (r_id, r_app, requested_name)
|
||||
)
|
||||
requests_by_name[requested_name] = r
|
||||
except Exception as e:
|
||||
result = "ERROR"
|
||||
main_logger.log(f"ERROR examining request {r.id}: " + str(e))
|
||||
main_logger.log(f"ERROR examining request {r_id}: " + str(e))
|
||||
finally:
|
||||
main_logger.log(f"DONE Examining request {r.id} with result {result}.")
|
||||
main_logger.log(f"DONE Examining request {r_id} with result {result}.")
|
||||
if result in ["ERROR"]:
|
||||
dump_known_requests(state_file, [r], status=result)
|
||||
|
||||
@@ -673,6 +689,7 @@ def command( # noqa: C901
|
||||
status = "ERROR"
|
||||
run_log_file = None
|
||||
run_reg_client = laconic
|
||||
build_logger = None
|
||||
try:
|
||||
run_id = (
|
||||
f"{r.id}-{str(time.time()).split('.')[0]}-"
|
||||
@@ -718,7 +735,8 @@ def command( # noqa: C901
|
||||
status = "DEPLOYED"
|
||||
except Exception as e:
|
||||
main_logger.log(f"ERROR {r.id}:" + str(e))
|
||||
build_logger.log("ERROR: " + str(e))
|
||||
if build_logger:
|
||||
build_logger.log("ERROR: " + str(e))
|
||||
finally:
|
||||
main_logger.log(f"DEPLOYING {r.id}: END - {status}")
|
||||
if build_logger:
|
||||
|
||||
@@ -64,7 +64,11 @@ def command( # noqa: C901
|
||||
):
|
||||
laconic = LaconicRegistryClient(laconic_config)
|
||||
if not payment_address:
|
||||
payment_address = laconic.whoami().address
|
||||
whoami_result = laconic.whoami()
|
||||
if whoami_result and whoami_result.address:
|
||||
payment_address = whoami_result.address
|
||||
else:
|
||||
raise ValueError("Could not determine payment address from laconic whoami")
|
||||
|
||||
pub_key = base64.b64encode(open(public_key_file, "rb").read()).decode("ASCII")
|
||||
hostname = urlparse(api_url).hostname
|
||||
|
||||
@@ -16,6 +16,7 @@ import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from typing import NoReturn
|
||||
import base64
|
||||
|
||||
import gnupg
|
||||
@@ -31,7 +32,7 @@ from stack_orchestrator.deploy.webapp.util import (
|
||||
from dotenv import dotenv_values
|
||||
|
||||
|
||||
def fatal(msg: str):
|
||||
def fatal(msg: str) -> NoReturn:
|
||||
print(msg, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -134,24 +135,30 @@ def command( # noqa: C901
|
||||
fatal(f"Unable to locate auction: {auction_id}")
|
||||
|
||||
# Check auction owner
|
||||
if auction.ownerAddress != laconic.whoami().address:
|
||||
whoami = laconic.whoami()
|
||||
if not whoami or not whoami.address:
|
||||
fatal("Unable to determine current account address")
|
||||
if auction.ownerAddress != whoami.address:
|
||||
fatal(f"Auction {auction_id} owner mismatch")
|
||||
|
||||
# Check auction kind
|
||||
if auction.kind != AUCTION_KIND_PROVIDER:
|
||||
auction_kind = auction.kind if auction else None
|
||||
if auction_kind != AUCTION_KIND_PROVIDER:
|
||||
fatal(
|
||||
f"Auction kind needs to be ${AUCTION_KIND_PROVIDER}, got {auction.kind}"
|
||||
f"Auction kind needs to be ${AUCTION_KIND_PROVIDER}, got {auction_kind}"
|
||||
)
|
||||
|
||||
# Check auction status
|
||||
if auction.status != AuctionStatus.COMPLETED:
|
||||
fatal(f"Auction {auction_id} not completed yet, status {auction.status}")
|
||||
auction_status = auction.status if auction else None
|
||||
if auction_status != AuctionStatus.COMPLETED:
|
||||
fatal(f"Auction {auction_id} not completed yet, status {auction_status}")
|
||||
|
||||
# Check that winner list is not empty
|
||||
if len(auction.winnerAddresses) == 0:
|
||||
winner_addresses = auction.winnerAddresses if auction else []
|
||||
if not winner_addresses or len(winner_addresses) == 0:
|
||||
fatal(f"Auction {auction_id} has no winners")
|
||||
|
||||
auction_winners = auction.winnerAddresses
|
||||
auction_winners = winner_addresses
|
||||
|
||||
# Get deployer record for all the auction winners
|
||||
for auction_winner in auction_winners:
|
||||
@@ -198,9 +205,12 @@ def command( # noqa: C901
|
||||
recip = gpg.list_keys()[0]["uids"][0]
|
||||
|
||||
# Wrap the config
|
||||
whoami_result = laconic.whoami()
|
||||
if not whoami_result or not whoami_result.address:
|
||||
fatal("Unable to determine current account address")
|
||||
config = {
|
||||
# Include account (and payment?) details
|
||||
"authorized": [laconic.whoami().address],
|
||||
"authorized": [whoami_result.address],
|
||||
"config": {"env": dict(dotenv_values(env_file))},
|
||||
}
|
||||
serialized = yaml.dump(config)
|
||||
@@ -227,12 +237,22 @@ def command( # noqa: C901
|
||||
if (not deployer) and len(deployer_record.names):
|
||||
target_deployer = deployer_record.names[0]
|
||||
|
||||
app_name = (
|
||||
app_record.attributes.name
|
||||
if app_record and app_record.attributes
|
||||
else "unknown"
|
||||
)
|
||||
app_version = (
|
||||
app_record.attributes.version
|
||||
if app_record and app_record.attributes
|
||||
else "unknown"
|
||||
)
|
||||
deployment_request = {
|
||||
"record": {
|
||||
"type": "ApplicationDeploymentRequest",
|
||||
"application": app,
|
||||
"version": "1.0.0",
|
||||
"name": f"{app_record.attributes.name}@{app_record.attributes.version}",
|
||||
"name": f"{app_name}@{app_version}",
|
||||
"deployer": target_deployer,
|
||||
"meta": {"when": str(datetime.utcnow())},
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ import yaml
|
||||
from stack_orchestrator.deploy.webapp.util import LaconicRegistryClient
|
||||
|
||||
|
||||
def fatal(msg: str):
|
||||
def fatal(msg: str) -> None:
|
||||
print(msg, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
sys.exit(1) # noqa: This function never returns
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -85,18 +85,17 @@ def command(
|
||||
if dry_run:
|
||||
undeployment_request["record"]["payment"] = "DRY_RUN"
|
||||
elif "auto" == make_payment:
|
||||
if "minimumPayment" in deployer_record.attributes:
|
||||
amount = int(
|
||||
deployer_record.attributes.minimumPayment.replace("alnt", "")
|
||||
)
|
||||
attrs = deployer_record.attributes if deployer_record else None
|
||||
if attrs and "minimumPayment" in attrs:
|
||||
amount = int(attrs.minimumPayment.replace("alnt", ""))
|
||||
else:
|
||||
amount = make_payment
|
||||
if amount:
|
||||
receipt = laconic.send_tokens(
|
||||
deployer_record.attributes.paymentAddress, amount
|
||||
)
|
||||
undeployment_request["record"]["payment"] = receipt.tx.hash
|
||||
print("Payment TX:", receipt.tx.hash)
|
||||
attrs = deployer_record.attributes if deployer_record else None
|
||||
if attrs and attrs.paymentAddress:
|
||||
receipt = laconic.send_tokens(attrs.paymentAddress, amount)
|
||||
undeployment_request["record"]["payment"] = receipt.tx.hash
|
||||
print("Payment TX:", receipt.tx.hash)
|
||||
elif use_payment:
|
||||
undeployment_request["record"]["payment"] = use_payment
|
||||
|
||||
|
||||
@@ -39,9 +39,12 @@ WEBAPP_PORT = 80
|
||||
def command(ctx, image, env_file, port):
|
||||
"""run the specified webapp container"""
|
||||
|
||||
env = {}
|
||||
env: dict[str, str] = {}
|
||||
if env_file:
|
||||
env = dotenv_values(env_file)
|
||||
# Filter out None values from dotenv
|
||||
for k, v in dotenv_values(env_file).items():
|
||||
if v is not None:
|
||||
env[k] = v
|
||||
|
||||
unique_cluster_descriptor = f"{image},{env}"
|
||||
hash = hashlib.md5(unique_cluster_descriptor.encode()).hexdigest()
|
||||
@@ -55,6 +58,11 @@ def command(ctx, image, env_file, port):
|
||||
compose_env_file=None,
|
||||
)
|
||||
|
||||
if not deployer:
|
||||
print("Failed to create deployer", file=click.get_text_stream("stderr"))
|
||||
ctx.exit(1)
|
||||
return # Unreachable, but helps type checker
|
||||
|
||||
ports = []
|
||||
if port:
|
||||
ports = [(port, WEBAPP_PORT)]
|
||||
@@ -72,10 +80,19 @@ def command(ctx, image, env_file, port):
|
||||
# Make configurable?
|
||||
webappPort = f"{WEBAPP_PORT}/tcp"
|
||||
# TODO: This assumes a Docker container object...
|
||||
if webappPort in container.network_settings.ports:
|
||||
# Check if container has network_settings (Docker container object)
|
||||
if (
|
||||
container
|
||||
and hasattr(container, "network_settings")
|
||||
and container.network_settings
|
||||
and hasattr(container.network_settings, "ports")
|
||||
and container.network_settings.ports
|
||||
and webappPort in container.network_settings.ports
|
||||
):
|
||||
mapping = container.network_settings.ports[webappPort][0]
|
||||
container_id = getattr(container, "id", "unknown")
|
||||
print(
|
||||
f"Image: {image}\n"
|
||||
f"ID: {container.id}\n"
|
||||
f"ID: {container_id}\n"
|
||||
f"URL: http://localhost:{mapping['HostPort']}"
|
||||
)
|
||||
|
||||
@@ -43,7 +43,13 @@ def process_app_removal_request(
|
||||
deployment_record = laconic.get_record(
|
||||
app_removal_request.attributes.deployment, require=True
|
||||
)
|
||||
assert deployment_record is not None # require=True ensures this
|
||||
assert deployment_record.attributes is not None
|
||||
|
||||
dns_record = laconic.get_record(deployment_record.attributes.dns, require=True)
|
||||
assert dns_record is not None # require=True ensures this
|
||||
assert dns_record.attributes is not None
|
||||
|
||||
deployment_dir = os.path.join(
|
||||
deployment_parent_dir, dns_record.attributes.name.lower()
|
||||
)
|
||||
@@ -57,17 +63,20 @@ def process_app_removal_request(
|
||||
|
||||
# Or of the original deployment 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),
|
||||
original_request = laconic.get_record(
|
||||
deployment_record.attributes.request, require=True
|
||||
)
|
||||
assert original_request is not None # require=True ensures this
|
||||
matched_owner = match_owner(app_removal_request, original_request)
|
||||
|
||||
if matched_owner:
|
||||
main_logger.log("Matched deployment ownership:", matched_owner)
|
||||
main_logger.log(f"Matched deployment ownership: {matched_owner}")
|
||||
else:
|
||||
deployment_id = deployment_record.id if deployment_record else "unknown"
|
||||
request_id = app_removal_request.id if app_removal_request else "unknown"
|
||||
raise Exception(
|
||||
"Unable to confirm ownership of deployment %s for removal request %s"
|
||||
% (deployment_record.id, app_removal_request.id)
|
||||
% (deployment_id, request_id)
|
||||
)
|
||||
|
||||
# TODO(telackey): Call the function directly. The easiest way to build
|
||||
@@ -80,13 +89,18 @@ def process_app_removal_request(
|
||||
result = subprocess.run(down_command)
|
||||
result.check_returncode()
|
||||
|
||||
deployer_name = (
|
||||
webapp_deployer_record.names[0]
|
||||
if webapp_deployer_record and webapp_deployer_record.names
|
||||
else ""
|
||||
)
|
||||
removal_record = {
|
||||
"record": {
|
||||
"type": "ApplicationDeploymentRemovalRecord",
|
||||
"version": "1.0.0",
|
||||
"request": app_removal_request.id,
|
||||
"deployment": deployment_record.id,
|
||||
"deployer": webapp_deployer_record.names[0],
|
||||
"request": app_removal_request.id if app_removal_request else "",
|
||||
"deployment": deployment_record.id if deployment_record else "",
|
||||
"deployer": deployer_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,11 +110,11 @@ def process_app_removal_request(
|
||||
laconic.publish(removal_record)
|
||||
|
||||
if delete_names:
|
||||
if deployment_record.names:
|
||||
if deployment_record and deployment_record.names:
|
||||
for name in deployment_record.names:
|
||||
laconic.delete_name(name)
|
||||
|
||||
if dns_record.names:
|
||||
if dns_record and dns_record.names:
|
||||
for name in dns_record.names:
|
||||
laconic.delete_name(name)
|
||||
|
||||
@@ -224,6 +238,8 @@ def command( # noqa: C901
|
||||
laconic_config, log_file=sys.stderr, mutex_lock_file=registry_lock_file
|
||||
)
|
||||
deployer_record = laconic.get_record(lrn, require=True)
|
||||
assert deployer_record is not None # require=True ensures this
|
||||
assert deployer_record.attributes is not None
|
||||
payment_address = deployer_record.attributes.paymentAddress
|
||||
main_logger.log(f"Payment address: {payment_address}")
|
||||
|
||||
@@ -236,6 +252,7 @@ def command( # noqa: C901
|
||||
sys.exit(2)
|
||||
|
||||
# Find deployment removal requests.
|
||||
requests = []
|
||||
# single request
|
||||
if request_id:
|
||||
main_logger.log(f"Retrieving request {request_id}...")
|
||||
@@ -259,32 +276,39 @@ def command( # noqa: C901
|
||||
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.reverse()
|
||||
# Filter out None values and sort by createTime
|
||||
valid_requests = [r for r in requests if r is not None]
|
||||
valid_requests.sort(key=lambda r: r.createTime if r else "")
|
||||
valid_requests.reverse()
|
||||
|
||||
# Find deployments.
|
||||
named_deployments = {}
|
||||
main_logger.log("Discovering app deployments...")
|
||||
for d in laconic.app_deployments(all=False):
|
||||
named_deployments[d.id] = d
|
||||
if d and d.id:
|
||||
named_deployments[d.id] = d
|
||||
|
||||
# Find removal requests.
|
||||
removals_by_deployment = {}
|
||||
removals_by_request = {}
|
||||
main_logger.log("Discovering deployment removals...")
|
||||
for r in laconic.app_deployment_removals():
|
||||
if r.attributes.deployment:
|
||||
if r and r.attributes and r.attributes.deployment:
|
||||
# TODO: should we handle CRNs?
|
||||
removals_by_deployment[r.attributes.deployment] = r
|
||||
|
||||
one_per_deployment = {}
|
||||
for r in requests:
|
||||
for r in valid_requests:
|
||||
if not r or not r.attributes:
|
||||
continue
|
||||
if not r.attributes.deployment:
|
||||
r_id = r.id if r else "unknown"
|
||||
main_logger.log(
|
||||
f"Skipping removal request {r.id} since it was a cancellation."
|
||||
f"Skipping removal request {r_id} since it was a cancellation."
|
||||
)
|
||||
elif r.attributes.deployment in one_per_deployment:
|
||||
main_logger.log(f"Skipping removal request {r.id} since it was superseded.")
|
||||
r_id = r.id if r else "unknown"
|
||||
main_logger.log(f"Skipping removal request {r_id} since it was superseded.")
|
||||
else:
|
||||
one_per_deployment[r.attributes.deployment] = r
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import uuid
|
||||
import yaml
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, List, Optional, TextIO
|
||||
|
||||
from stack_orchestrator.deploy.webapp.registry_mutex import registry_mutex
|
||||
|
||||
@@ -41,27 +42,35 @@ AUCTION_KIND_PROVIDER = "provider"
|
||||
|
||||
|
||||
class AttrDict(dict):
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super(AttrDict, self).__init__(*args, **kwargs)
|
||||
self.__dict__ = self
|
||||
|
||||
def __getattribute__(self, attr):
|
||||
def __getattribute__(self, attr: str) -> Any:
|
||||
__dict__ = super(AttrDict, self).__getattribute__("__dict__")
|
||||
if attr in __dict__:
|
||||
v = super(AttrDict, self).__getattribute__(attr)
|
||||
if isinstance(v, dict):
|
||||
return AttrDict(v)
|
||||
return v
|
||||
return super(AttrDict, self).__getattribute__(attr)
|
||||
|
||||
def __getattr__(self, attr: str) -> Any:
|
||||
# This method is called when attribute is not found
|
||||
# Return None for missing attributes (matches original behavior)
|
||||
return None
|
||||
|
||||
|
||||
class TimedLogger:
|
||||
def __init__(self, id="", file=None):
|
||||
def __init__(self, id: str = "", file: Optional[TextIO] = None) -> None:
|
||||
self.start = datetime.datetime.now()
|
||||
self.last = self.start
|
||||
self.id = id
|
||||
self.file = file
|
||||
|
||||
def log(self, msg, show_step_time=True, show_total_time=False):
|
||||
def log(
|
||||
self, msg: str, show_step_time: bool = True, show_total_time: bool = False
|
||||
) -> None:
|
||||
prefix = f"{datetime.datetime.utcnow()} - {self.id}"
|
||||
if show_step_time:
|
||||
prefix += f" - {datetime.datetime.now() - self.last} (step)"
|
||||
@@ -79,7 +88,7 @@ def load_known_requests(filename):
|
||||
return {}
|
||||
|
||||
|
||||
def logged_cmd(log_file, *vargs):
|
||||
def logged_cmd(log_file: Optional[TextIO], *vargs: str) -> str:
|
||||
result = None
|
||||
try:
|
||||
if log_file:
|
||||
@@ -88,17 +97,22 @@ def logged_cmd(log_file, *vargs):
|
||||
result.check_returncode()
|
||||
return result.stdout.decode()
|
||||
except Exception as err:
|
||||
if result:
|
||||
print(result.stderr.decode(), file=log_file)
|
||||
else:
|
||||
print(str(err), file=log_file)
|
||||
if log_file:
|
||||
if result:
|
||||
print(result.stderr.decode(), file=log_file)
|
||||
else:
|
||||
print(str(err), file=log_file)
|
||||
raise err
|
||||
|
||||
|
||||
def match_owner(recordA, *records):
|
||||
def match_owner(
|
||||
recordA: Optional[AttrDict], *records: Optional[AttrDict]
|
||||
) -> Optional[str]:
|
||||
if not recordA or not recordA.owners:
|
||||
return None
|
||||
for owner in recordA.owners:
|
||||
for otherRecord in records:
|
||||
if owner in otherRecord.owners:
|
||||
if otherRecord and otherRecord.owners and owner in otherRecord.owners:
|
||||
return owner
|
||||
return None
|
||||
|
||||
@@ -226,25 +240,27 @@ class LaconicRegistryClient:
|
||||
]
|
||||
|
||||
# Most recent records first
|
||||
results.sort(key=lambda r: r.createTime)
|
||||
results.sort(key=lambda r: r.createTime or "")
|
||||
results.reverse()
|
||||
self._add_to_cache(results)
|
||||
|
||||
return results
|
||||
|
||||
def _add_to_cache(self, records):
|
||||
def _add_to_cache(self, records: List[AttrDict]) -> None:
|
||||
if not records:
|
||||
return
|
||||
|
||||
for p in records:
|
||||
self.cache["name_or_id"][p.id] = p
|
||||
if p.id:
|
||||
self.cache["name_or_id"][p.id] = p
|
||||
if p.names:
|
||||
for lrn in p.names:
|
||||
self.cache["name_or_id"][lrn] = p
|
||||
if p.attributes and p.attributes.type:
|
||||
if p.attributes.type not in self.cache:
|
||||
self.cache[p.attributes.type] = []
|
||||
self.cache[p.attributes.type].append(p)
|
||||
attr_type = p.attributes.type
|
||||
if attr_type not in self.cache:
|
||||
self.cache[attr_type] = []
|
||||
self.cache[attr_type].append(p)
|
||||
|
||||
def resolve(self, name):
|
||||
if not name:
|
||||
@@ -556,26 +572,36 @@ def determine_base_container(clone_dir, app_type="webapp"):
|
||||
return base_container
|
||||
|
||||
|
||||
def build_container_image(app_record, tag, extra_build_args=None, logger=None):
|
||||
def build_container_image(
|
||||
app_record: Optional[AttrDict],
|
||||
tag: str,
|
||||
extra_build_args: Optional[List[str]] = None,
|
||||
logger: Optional[TimedLogger] = None,
|
||||
) -> None:
|
||||
if app_record is None:
|
||||
raise ValueError("app_record cannot be None")
|
||||
if extra_build_args is None:
|
||||
extra_build_args = []
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
|
||||
# TODO: determine if this code could be calling into the Python git
|
||||
# library like setup-repositories
|
||||
log_file = logger.file if logger else None
|
||||
try:
|
||||
record_id = app_record["id"]
|
||||
ref = app_record.attributes.repository_ref
|
||||
repo = random.choice(app_record.attributes.repository)
|
||||
clone_dir = os.path.join(tmpdir, record_id)
|
||||
|
||||
logger.log(f"Cloning repository {repo} to {clone_dir} ...")
|
||||
if logger:
|
||||
logger.log(f"Cloning repository {repo} to {clone_dir} ...")
|
||||
# Set github credentials if present running a command like:
|
||||
# git config --global url."https://${TOKEN}:@github.com/".insteadOf
|
||||
# "https://github.com/"
|
||||
github_token = os.environ.get("DEPLOYER_GITHUB_TOKEN")
|
||||
if github_token:
|
||||
logger.log("Github token detected, setting it in the git environment")
|
||||
if logger:
|
||||
logger.log("Github token detected, setting it in the git environment")
|
||||
git_config_args = [
|
||||
"git",
|
||||
"config",
|
||||
@@ -583,9 +609,7 @@ def build_container_image(app_record, tag, extra_build_args=None, logger=None):
|
||||
f"url.https://{github_token}:@github.com/.insteadOf",
|
||||
"https://github.com/",
|
||||
]
|
||||
result = subprocess.run(
|
||||
git_config_args, stdout=logger.file, stderr=logger.file
|
||||
)
|
||||
result = subprocess.run(git_config_args, stdout=log_file, stderr=log_file)
|
||||
result.check_returncode()
|
||||
if ref:
|
||||
# TODO: Determing branch or hash, and use depth 1 if we can.
|
||||
@@ -596,30 +620,32 @@ def build_container_image(app_record, tag, extra_build_args=None, logger=None):
|
||||
subprocess.check_call(
|
||||
["git", "clone", repo, clone_dir],
|
||||
env=git_env,
|
||||
stdout=logger.file,
|
||||
stderr=logger.file,
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.log(f"git clone failed. Is the repository {repo} private?")
|
||||
if logger:
|
||||
logger.log(f"git clone failed. Is the repository {repo} private?")
|
||||
raise e
|
||||
try:
|
||||
subprocess.check_call(
|
||||
["git", "checkout", ref],
|
||||
cwd=clone_dir,
|
||||
env=git_env,
|
||||
stdout=logger.file,
|
||||
stderr=logger.file,
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.log(f"git checkout failed. Does ref {ref} exist?")
|
||||
if logger:
|
||||
logger.log(f"git checkout failed. Does ref {ref} exist?")
|
||||
raise e
|
||||
else:
|
||||
# TODO: why is this code different vs the branch above (run vs check_call,
|
||||
# and no prompt disable)?
|
||||
result = subprocess.run(
|
||||
["git", "clone", "--depth", "1", repo, clone_dir],
|
||||
stdout=logger.file,
|
||||
stderr=logger.file,
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
)
|
||||
result.check_returncode()
|
||||
|
||||
@@ -627,7 +653,8 @@ def build_container_image(app_record, tag, extra_build_args=None, logger=None):
|
||||
clone_dir, app_record.attributes.app_type
|
||||
)
|
||||
|
||||
logger.log("Building webapp ...")
|
||||
if logger:
|
||||
logger.log("Building webapp ...")
|
||||
build_command = [
|
||||
sys.argv[0],
|
||||
"--verbose",
|
||||
@@ -643,10 +670,10 @@ def build_container_image(app_record, tag, extra_build_args=None, logger=None):
|
||||
build_command.append("--extra-build-args")
|
||||
build_command.append(" ".join(extra_build_args))
|
||||
|
||||
result = subprocess.run(build_command, stdout=logger.file, stderr=logger.file)
|
||||
result = subprocess.run(build_command, stdout=log_file, stderr=log_file)
|
||||
result.check_returncode()
|
||||
finally:
|
||||
logged_cmd(logger.file, "rm", "-rf", tmpdir)
|
||||
logged_cmd(log_file, "rm", "-rf", tmpdir)
|
||||
|
||||
|
||||
def push_container_image(deployment_dir, logger):
|
||||
@@ -809,8 +836,12 @@ def skip_by_tag(r, include_tags, exclude_tags):
|
||||
|
||||
|
||||
def confirm_payment(
|
||||
laconic: LaconicRegistryClient, record, payment_address, min_amount, logger
|
||||
):
|
||||
laconic: LaconicRegistryClient,
|
||||
record: AttrDict,
|
||||
payment_address: str,
|
||||
min_amount: int,
|
||||
logger: TimedLogger,
|
||||
) -> bool:
|
||||
req_owner = laconic.get_owner(record)
|
||||
if req_owner == payment_address:
|
||||
# No need to confirm payment if the sender and recipient are the same account.
|
||||
@@ -846,7 +877,8 @@ def confirm_payment(
|
||||
)
|
||||
return False
|
||||
|
||||
pay_denom = "".join([i for i in tx.amount if not i.isdigit()])
|
||||
tx_amount = tx.amount or ""
|
||||
pay_denom = "".join([i for i in tx_amount if not i.isdigit()])
|
||||
if pay_denom != "alnt":
|
||||
logger.log(
|
||||
f"{record.id}: {pay_denom} in tx {tx.hash} is not an expected "
|
||||
@@ -854,7 +886,7 @@ def confirm_payment(
|
||||
)
|
||||
return False
|
||||
|
||||
pay_amount = int("".join([i for i in tx.amount if i.isdigit()]))
|
||||
pay_amount = int("".join([i for i in tx_amount if i.isdigit()]) or "0")
|
||||
if pay_amount < min_amount:
|
||||
logger.log(
|
||||
f"{record.id}: payment amount {tx.amount} is less than minimum {min_amount}"
|
||||
@@ -870,7 +902,8 @@ def confirm_payment(
|
||||
used_request = laconic.get_record(used[0].attributes.request, require=True)
|
||||
|
||||
# Check that payment was used for deployment of same application
|
||||
if record.attributes.application != used_request.attributes.application:
|
||||
used_app = used_request.attributes.application if used_request else None
|
||||
if record.attributes.application != used_app:
|
||||
logger.log(
|
||||
f"{record.id}: payment {tx.hash} already used on a different "
|
||||
f"application deployment {used}"
|
||||
@@ -890,8 +923,12 @@ def confirm_payment(
|
||||
|
||||
|
||||
def confirm_auction(
|
||||
laconic: LaconicRegistryClient, record, deployer_lrn, payment_address, logger
|
||||
):
|
||||
laconic: LaconicRegistryClient,
|
||||
record: AttrDict,
|
||||
deployer_lrn: str,
|
||||
payment_address: str,
|
||||
logger: TimedLogger,
|
||||
) -> bool:
|
||||
auction_id = record.attributes.auction
|
||||
auction = laconic.get_auction(auction_id)
|
||||
|
||||
@@ -906,7 +943,9 @@ def confirm_auction(
|
||||
auction_app = laconic.get_record(
|
||||
auction_records_by_id[0].attributes.application, require=True
|
||||
)
|
||||
if requested_app.id != auction_app.id:
|
||||
requested_app_id = requested_app.id if requested_app else None
|
||||
auction_app_id = auction_app.id if auction_app else None
|
||||
if requested_app_id != auction_app_id:
|
||||
logger.log(
|
||||
f"{record.id}: requested application {record.attributes.application} "
|
||||
f"does not match application from auction record "
|
||||
|
||||
Reference in New Issue
Block a user