Skip to content

Commit

Permalink
Support uploaded config, add 'publish-webapp-deployer' and 'request-w…
Browse files Browse the repository at this point in the history
…ebapp-deployment' commands (#938)

This adds two new commands: `publish-webapp-deployer` and `request-webapp-deployment`.

`publish-webapp-deployer` creates a `WebappDeployer` record, which provides information to requestors like the API URL, minimum required payment, payment address, and public key to use for encrypting config.

```
$ laconic-so publish-deployer-to-registry \
  --laconic-config ~/.laconic/laconic.yml \
  --api-url https://webapp-deployer-api.dev.vaasl.io \
  --public-key-file webapp-deployer-api.dev.vaasl.io.pgp.pub  \
  --lrn lrn://laconic/deployers/webapp-deployer-api.dev.vaasl.io  \
  --min-required-payment 100000
```

`request-webapp-deployment` simplifies publishing a `WebappDeploymentRequest` and can also handle automatic payment, and encryption and upload of configuration.

```
$ laconic-so request-webapp-deployment \
  --laconic-config ~/.laconic/laconic.yml \
  --deployer lrn://laconic/deployers/webapp-deployer-api.dev.vaasl.io \
  --app lrn://cerc-io/applications/[email protected] \
  --env-file ~/yaml/hello.env \
  --make-payment auto
```

Related changes are included for the deploy/undeploy commands for decrypting and using config, using the payment address from the WebappDeployer record, etc.

Reviewed-on: https://git.vdb.to/cerc-io/stack-orchestrator/pulls/938
  • Loading branch information
Thomas E Lackey committed Aug 27, 2024
1 parent 33d395e commit fa21ff2
Show file tree
Hide file tree
Showing 8 changed files with 410 additions and 48 deletions.
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ tomli==2.0.1
validators==0.22.0
kubernetes>=28.1.0
humanfriendly>=10.0
python-gnupg>=0.5.2
requests>=2.3.2
5 changes: 3 additions & 2 deletions stack_orchestrator/deploy/k8s/cluster_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# along with this program. If not, see <http:#www.gnu.org/licenses/>.

import os
import base64

from kubernetes import client
from typing import Any, List, Set
Expand Down Expand Up @@ -260,12 +261,12 @@ def get_configmaps(self):
for f in os.listdir(cfg_map_path):
full_path = os.path.join(cfg_map_path, f)
if os.path.isfile(full_path):
data[f] = open(full_path, 'rt').read()
data[f] = base64.b64encode(open(full_path, 'rb').read()).decode('ASCII')

spec = client.V1ConfigMap(
metadata=client.V1ObjectMeta(name=f"{self.app_name}-{cfg_map_name}",
labels={"configmap-label": cfg_map_name}),
data=data
binary_data=data
)
result.append(spec)
return result
Expand Down
113 changes: 89 additions & 24 deletions stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
import tempfile
import time
import uuid
import yaml

import click
import gnupg

from stack_orchestrator.deploy.images import remote_image_exists
from stack_orchestrator.deploy.webapp import deploy_webapp
from stack_orchestrator.deploy.webapp.util import (
AttrDict,
LaconicRegistryClient,
TimedLogger,
build_container_image,
Expand Down Expand Up @@ -55,7 +58,10 @@ def process_app_deployment_request(
force_rebuild,
fqdn_policy,
recreate_on_deploy,
payment_address,
webapp_deployer_record,
gpg,
private_key_passphrase,
config_upload_dir,
logger,
):
logger.log("BEGIN - process_app_deployment_request")
Expand Down Expand Up @@ -107,14 +113,31 @@ def process_app_deployment_request(
)

# 4. get build and runtime config from request
env = {}
if app_deployment_request.attributes.config:
if "ref" in app_deployment_request.attributes.config:
with open(
f"{config_upload_dir}/{app_deployment_request.attributes.config.ref}",
"rb",
) as file:
record_owner = laconic.get_owner(app_deployment_request)
decrypted = gpg.decrypt_file(file, passphrase=private_key_passphrase)
parsed = AttrDict(yaml.safe_load(decrypted.data))
if record_owner not in parsed.authorized:
raise Exception(
f"{record_owner} not authorized to access config {app_deployment_request.attributes.config.ref}"
)
if "env" in parsed.config:
env.update(parsed.config.env)

if "env" in app_deployment_request.attributes.config:
env.update(app_deployment_request.attributes.config.env)

env_filename = None
if (
app_deployment_request.attributes.config
and "env" in app_deployment_request.attributes.config
):
if env:
env_filename = tempfile.mktemp()
with open(env_filename, "w") as file:
for k, v in app_deployment_request.attributes.config["env"].items():
for k, v in env.items():
file.write("%s=%s\n" % (k, shlex.quote(str(v))))

# 5. determine new or existing deployment
Expand Down Expand Up @@ -227,7 +250,7 @@ def process_app_deployment_request(
dns_lrn,
deployment_dir,
app_deployment_request,
payment_address,
webapp_deployer_record,
logger,
)
logger.log("Publication complete.")
Expand Down Expand Up @@ -285,8 +308,12 @@ def dump_known_requests(filename, requests, status="SEEN"):
help="How to handle requests with an FQDN: prohibit, allow, preexisting",
default="prohibit",
)
@click.option("--record-namespace-dns", help="eg, lrn://laconic/dns")
@click.option("--record-namespace-deployments", help="eg, lrn://laconic/deployments")
@click.option("--record-namespace-dns", help="eg, lrn://laconic/dns", required=True)
@click.option(
"--record-namespace-deployments",
help="eg, lrn://laconic/deployments",
required=True,
)
@click.option(
"--dry-run", help="Don't do anything, just report what would be done.", is_flag=True
)
Expand All @@ -313,21 +340,29 @@ def dump_known_requests(filename, requests, status="SEEN"):
)
@click.option(
"--min-required-payment",
help="Requests must have a minimum payment to be processed",
help="Requests must have a minimum payment to be processed (in alnt)",
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("--lrn", help="The LRN of this deployer.", required=True)
@click.option(
"--all-requests",
help="Handle requests addressed to anyone (by default only requests to"
"my payment address are examined).",
is_flag=True,
)
@click.option(
"--config-upload-dir",
help="The directory containing uploaded config.",
required=True,
)
@click.option(
"--private-key-file", help="The private key for decrypting config.", required=True
)
@click.option(
"--private-key-passphrase",
help="The passphrase for the private key.",
required=True,
)
@click.pass_context
def command( # noqa: C901
ctx,
Expand All @@ -350,7 +385,10 @@ def command( # noqa: C901
recreate_on_deploy,
log_dir,
min_required_payment,
payment_address,
lrn,
config_upload_dir,
private_key_file,
private_key_passphrase,
all_requests,
):
if request_id and discover:
Expand Down Expand Up @@ -384,6 +422,18 @@ def command( # noqa: C901
)
sys.exit(2)

tempdir = tempfile.mkdtemp()
gpg = gnupg.GPG(gnupghome=tempdir)

# Import the deployer's public key
result = gpg.import_keys(open(private_key_file, "rb").read())
if 1 != result.imported:
print(
f"Failed to load private key file: {private_key_file}.",
file=sys.stderr,
)
sys.exit(2)

main_logger = TimedLogger(file=sys.stderr)

try:
Expand All @@ -392,11 +442,17 @@ def command( # noqa: C901
exclude_tags = [tag.strip() for tag in exclude_tags.split(",") if tag]

laconic = LaconicRegistryClient(laconic_config, log_file=sys.stderr)
if not payment_address:
payment_address = laconic.whoami().address

webapp_deployer_record = laconic.get_record(lrn, require=True)
payment_address = webapp_deployer_record.attributes.paymentAddress
main_logger.log(f"Payment address: {payment_address}")

if min_required_payment and not payment_address:
print(
f"Minimum payment required, but no payment address listed for deployer: {lrn}.",
file=sys.stderr,
)
sys.exit(2)

# Find deployment requests.
# single request
if request_id:
Expand All @@ -408,7 +464,7 @@ def command( # noqa: C901
if all_requests:
requests = laconic.app_deployment_requests()
else:
requests = laconic.app_deployment_requests({"to": payment_address})
requests = laconic.app_deployment_requests({"deployer": lrn})

if only_update_state:
if not dry_run:
Expand Down Expand Up @@ -487,7 +543,7 @@ def command( # noqa: C901
if all_requests:
deployments = laconic.app_deployments()
else:
deployments = laconic.app_deployments({"by": payment_address})
deployments = laconic.app_deployments({"deployer": lrn})
deployments_by_request = {}
for d in deployments:
if d.attributes.request:
Expand Down Expand Up @@ -530,7 +586,11 @@ def command( # noqa: C901
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
laconic,
r,
payment_address,
min_required_payment,
main_logger,
):
main_logger.log(f"{r.id}: Payment confirmed.")
requests_to_execute.append(r)
Expand Down Expand Up @@ -583,7 +643,10 @@ def command( # noqa: C901
force_rebuild,
fqdn_policy,
recreate_on_deploy,
payment_address,
webapp_deployer_record,
gpg,
private_key_passphrase,
config_upload_dir,
build_logger,
)
status = "DEPLOYED"
Expand All @@ -604,3 +667,5 @@ def command( # noqa: C901
except Exception as e:
main_logger.log("UNCAUGHT ERROR:" + str(e))
raise e
finally:
shutil.rmtree(tempdir)
91 changes: 91 additions & 0 deletions stack_orchestrator/deploy/webapp/publish_webapp_deployer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright ©2023 Vulcanize
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http:#www.gnu.org/licenses/>.

import base64
import click
import sys
import yaml

from urllib.parse import urlparse

from stack_orchestrator.deploy.webapp.util import LaconicRegistryClient


@click.command()
@click.option(
"--laconic-config", help="Provide a config file for laconicd", required=True
)
@click.option("--api-url", help="The API URL of the deployer.", required=True)
@click.option(
"--public-key-file",
help="The public key to use. This should be a binary file.",
required=True,
)
@click.option(
"--lrn", help="eg, lrn://laconic/deployers/my.deployer.name", required=True
)
@click.option(
"--payment-address",
help="The address to which payments should be made. "
"Default is the current laconic account.",
default=None,
)
@click.option(
"--min-required-payment",
help="List the minimum required payment (in alnt) to process a deployment request.",
default=0,
)
@click.option(
"--dry-run",
help="Don't publish anything, just report what would be done.",
is_flag=True,
)
@click.pass_context
def command( # noqa: C901
ctx,
laconic_config,
api_url,
public_key_file,
lrn,
payment_address,
min_required_payment,
dry_run,
):
laconic = LaconicRegistryClient(laconic_config)
if not payment_address:
payment_address = laconic.whoami().address

pub_key = base64.b64encode(open(public_key_file, "rb").read()).decode("ASCII")
hostname = urlparse(api_url).hostname
webapp_deployer_record = {
"record": {
"type": "WebappDeployer",
"version": "1.0.0",
"apiUrl": api_url,
"name": hostname,
"publicKey": pub_key,
"paymentAddress": payment_address,
}
}

if min_required_payment:
webapp_deployer_record["record"][
"minimumPayment"
] = f"{min_required_payment}alnt"

if dry_run:
yaml.dump(webapp_deployer_record, sys.stdout)
return

laconic.publish(webapp_deployer_record, [lrn])
Loading

0 comments on commit fa21ff2

Please sign in to comment.