Compare commits

..
13 Commits
Author SHA1 Message Date
telackey 578fd933a7 lint 2024-08-20 19:10:27 -05:00
telackey 35d23ca858 lint 2024-08-20 19:01:58 -05:00
telackey e06de5f4b7 Include payment in removal record 2024-08-20 18:19:22 -05:00
telackey 6e64b60295 Set payment address 2024-08-20 18:02:09 -05:00
telackey 3205478878 Undeploy 2024-08-20 16:58:34 -05:00
telackey 916724f1e2 Undeploy 2024-08-20 16:57:42 -05:00
telackey 56b010f512 Not a list 2024-08-20 14:58:06 -05:00
telackey 023a640252 lint 2024-08-20 11:41:34 -05:00
telackey 09c9214e4c log 2024-08-19 19:39:43 -05:00
telackey 2f1cde16b7 Update in re cerc-io/laconic-registry-cli#78 2024-08-19 19:35:44 -05:00
telackey b449d88b6c Include payment info in deployment record. 2024-08-16 22:33:23 -05:00
telackey 07030044ec get_tx() 2024-08-16 21:59:52 -05:00
telackey 0a9d68f4e8 WIP: Require payment for app deployment requests. 2024-08-16 21:51:15 -05:00
19 changed files with 52 additions and 889 deletions
-2
View File
@@ -11,5 +11,3 @@ tomli==2.0.1
validators==0.22.0
kubernetes>=28.1.0
humanfriendly>=10.0
python-gnupg>=0.5.2
requests>=2.3.2
@@ -240,4 +240,4 @@ fi
time $CERC_BUILD_TOOL run cerc_compile || exit 1
exit 0
exit 0
@@ -1,51 +0,0 @@
# Originally from: https://github.com/devcontainers/images/blob/main/src/javascript-node/.devcontainer/Dockerfile
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=20-bullseye-slim
FROM node:${VARIANT}
ARG USERNAME=node
ARG NPM_GLOBAL=/usr/local/share/npm-global
# Add NPM global to PATH.
ENV PATH=${NPM_GLOBAL}/bin:${PATH}
# Prevents npm from printing version warnings
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
RUN \
# Configure global npm install location, use group to adapt to UID/GID changes
if ! cat /etc/group | grep -e "^npm:" > /dev/null 2>&1; then groupadd -r npm; fi \
&& usermod -a -G npm ${USERNAME} \
&& umask 0002 \
&& mkdir -p ${NPM_GLOBAL} \
&& touch /usr/local/etc/npmrc \
&& chown ${USERNAME}:npm ${NPM_GLOBAL} /usr/local/etc/npmrc \
&& chmod g+s ${NPM_GLOBAL} \
&& npm config -g set prefix ${NPM_GLOBAL} \
&& su ${USERNAME} -c "npm config -g set prefix ${NPM_GLOBAL}" \
# Install eslint
&& su ${USERNAME} -c "umask 0002 && npm install -g eslint" \
# Install semver
&& su ${USERNAME} -c "umask 0002 && npm install -g semver" \
# Install pnpm
&& su ${USERNAME} -c "umask 0002 && npm install -g pnpm" \
# Install bun
&& su ${USERNAME} -c "umask 0002 && npm install -g bun@1.1.x" \
&& npm cache clean --force > /dev/null 2>&1
# [Optional] Uncomment this section to install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends jq gettext-base procps
# [Optional] Uncomment if you want to install more global node modules
# RUN su node -c "npm install -g <your-package-list-here>"
# We do this to get a yq binary from the published container, for the correct architecture we're building here
COPY --from=docker.io/mikefarah/yq:latest /usr/bin/yq /usr/local/bin/yq
# Expose port for http
EXPOSE 80
COPY /scripts /scripts
# Default command sleeps forever so docker doesn't kill it
ENTRYPOINT ["/scripts/start-serving-app.sh"]
@@ -1,9 +0,0 @@
FROM cerc/nextjs-base:local
ARG CERC_NEXT_VERSION=keep
ARG CERC_BUILD_TOOL
WORKDIR /app
COPY . .
RUN rm -rf node_modules build .next*
RUN /scripts/build-app.sh /app
@@ -1,35 +0,0 @@
#!/usr/bin/env bash
# Build cerc/laconic-registry-cli
source ${CERC_CONTAINER_BASE_DIR}/build-base.sh
# See: https://stackoverflow.com/a/246128/1701505
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
CERC_CONTAINER_BUILD_WORK_DIR=${CERC_CONTAINER_BUILD_WORK_DIR:-$SCRIPT_DIR}
CERC_CONTAINER_BUILD_DOCKERFILE=${CERC_CONTAINER_BUILD_DOCKERFILE:-$SCRIPT_DIR/Dockerfile}
CERC_CONTAINER_BUILD_TAG=${CERC_CONTAINER_BUILD_TAG:-cerc/nextjs-base:local}
docker build -t $CERC_CONTAINER_BUILD_TAG ${build_command_args} -f $CERC_CONTAINER_BUILD_DOCKERFILE $CERC_CONTAINER_BUILD_WORK_DIR
rc=$?
if [ $rc -ne 0 ]; then
echo "BUILD FAILED" 1>&2
exit $rc
fi
if [ "$CERC_CONTAINER_BUILD_TAG" != "cerc/nextjs-base:local" ]; then
cat <<EOF
#################################################################
Built host container for $CERC_CONTAINER_BUILD_WORK_DIR with tag:
$CERC_CONTAINER_BUILD_TAG
To test locally run:
laconic-so run-webapp --image $CERC_CONTAINER_BUILD_TAG --env-file /path/to/environment.env
EOF
fi
@@ -1,52 +0,0 @@
#!/bin/bash
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x
fi
WORK_DIR="${1:-./}"
SRC_DIR="${2:-.next}"
TRG_DIR="${3:-.next-r}"
CERC_BUILD_TOOL="${CERC_BUILD_TOOL}"
if [ -z "$CERC_BUILD_TOOL" ]; then
if [ -f "pnpm-lock.yaml" ]; then
CERC_BUILD_TOOL=pnpm
elif [ -f "yarn.lock" ]; then
CERC_BUILD_TOOL=yarn
elif [ -f "bun.lockb" ]; then
CERC_BUILD_TOOL=bun
else
CERC_BUILD_TOOL=npm
fi
fi
cd "${WORK_DIR}" || exit 1
rm -rf "$TRG_DIR"
mkdir -p "$TRG_DIR"
cp -rp "$SRC_DIR" "$TRG_DIR/"
if [ -f ".env" ]; then
TMP_ENV=`mktemp`
declare -px > $TMP_ENV
set -a
source .env
source $TMP_ENV
set +a
rm -f $TMP_ENV
fi
for f in $(find . -type f \( -regex '.*.html?' -or -regex ".*.[tj]s\(x\|on\)?$" \) | grep -v 'node_modules' | grep -v '.git'); do
for e in $(cat "${f}" | tr -s '[:blank:]' '\n' | tr -s '["/\\{},();]' '\n' | tr -s "[']" '\n' | egrep -o -e '^CERC_RUNTIME_ENV_.+$' -e '^LACONIC_HOSTED_CONFIG_.+$'); do
orig_name=$(echo -n "${e}" | sed 's/"//g')
cur_name=$(echo -n "${orig_name}" | sed 's/CERC_RUNTIME_ENV_//g')
cur_val=$(echo -n "\$${cur_name}" | envsubst)
if [ "$CERC_RETAIN_ENV_QUOTES" != "true" ]; then
cur_val=$(sed "s/^[\"']//" <<< "$cur_val" | sed "s/[\"']//")
fi
esc_val=$(sed 's/[&/\]/\\&/g' <<< "$cur_val")
echo "$f: $cur_name=$cur_val"
sed -i "s/$orig_name/$esc_val/g" $f
done
done
@@ -1,201 +0,0 @@
#!/bin/bash
set -euo pipefail
if [ -n "${CERC_SCRIPT_DEBUG:-}" ]; then
set -x
fi
# Determine the build tool
CERC_BUILD_TOOL="${CERC_BUILD_TOOL:-}"
if [ -z "$CERC_BUILD_TOOL" ]; then
if [ -f "pnpm-lock.yaml" ]; then
CERC_BUILD_TOOL=pnpm
elif [ -f "yarn.lock" ]; then
CERC_BUILD_TOOL=yarn
elif [ -f "bun.lockb" ]; then
CERC_BUILD_TOOL=bun
else
CERC_BUILD_TOOL=npm
fi
fi
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
WORK_DIR="${1:-/app}"
cd "${WORK_DIR}" || exit 1
# Get the app's Next.js version
CERC_NEXT_VERSION=$(jq -r '.dependencies.next' package.json)
echo "Using Next.js version: $CERC_NEXT_VERSION"
# Determine webpack version based on Next.js version (unused if already installed)
determine_webpack_version() {
local next_version=$1
local major_version=${next_version%%.*}
case $major_version in
13)
echo "5.75.0"
;;
14)
echo "5.88.0"
;;
*)
echo "5.93.0" # Default to latest stable version if unknown
;;
esac
}
# Check if webpack is already in package.json
if CERC_WEBPACK_VERSION=$(jq -r '.dependencies.webpack // .devDependencies.webpack // empty' package.json); then
if [ -n "$CERC_WEBPACK_VERSION" ]; then
echo "Using existing webpack version: $CERC_WEBPACK_VERSION"
else
CERC_WEBPACK_VERSION=$(determine_webpack_version "$CERC_NEXT_VERSION")
echo "Determined webpack version based on Next.js: $CERC_WEBPACK_VERSION"
# Add webpack to devDependencies
jq ".devDependencies.webpack = \"$CERC_WEBPACK_VERSION\"" package.json > package.json.tmp && mv package.json.tmp package.json
fi
else
CERC_WEBPACK_VERSION=$(determine_webpack_version "$CERC_NEXT_VERSION")
echo "Determined webpack version based on Next.js: $CERC_WEBPACK_VERSION"
# Add webpack to devDependencies
jq ".devDependencies.webpack = \"$CERC_WEBPACK_VERSION\"" package.json > package.json.tmp && mv package.json.tmp package.json
fi
# Determine the Next.js config file
if [ -f "next.config.mjs" ]; then
NEXT_CONFIG_JS="next.config.mjs"
IMPORT_OR_REQUIRE="import"
else
NEXT_CONFIG_JS="next.config.js"
IMPORT_OR_REQUIRE="require"
fi
# If this file doesn't exist at all, we'll get errors below.
if [ ! -f "${NEXT_CONFIG_JS}" ]; then
touch ${NEXT_CONFIG_JS}
fi
# Backup the original config if not already done
if [ ! -f "next.config.dist" ]; then
cp "$NEXT_CONFIG_JS" next.config.dist
fi
# Install js-beautify if not present
command -v js-beautify >/dev/null || npm i -g js-beautify
# Beautify the config file
js-beautify "$NEXT_CONFIG_JS" > "${NEXT_CONFIG_JS}.pretty"
mv "${NEXT_CONFIG_JS}.pretty" "$NEXT_CONFIG_JS"
# Add webpack import/require if not present
if [ "$IMPORT_OR_REQUIRE" = "import" ]; then
if ! grep -q "import.*webpack" "$NEXT_CONFIG_JS"; then
sed -i '1iimport webpack from "webpack";' "$NEXT_CONFIG_JS"
fi
if ! grep -q "import.*createRequire" "$NEXT_CONFIG_JS"; then
sed -i '2iimport { createRequire } from "module";\nconst require = createRequire(import.meta.url);' "$NEXT_CONFIG_JS"
fi
else
if ! grep -q "require.*webpack" "$NEXT_CONFIG_JS"; then
sed -i '1iconst webpack = require("webpack");' "$NEXT_CONFIG_JS"
fi
fi
# Add environment mapping logic if not present
if ! grep -q "let envMap;" "$NEXT_CONFIG_JS"; then
cat << 'EOF' >> "$NEXT_CONFIG_JS"
let envMap;
try {
envMap = require('./.env-list.json').reduce((a, v) => {
a[v] = JSON.stringify(`CERC_RUNTIME_ENV_${v.split(/\./).pop()}`);
return a;
}, {});
} catch (e) {
console.error('Error loading .env-list.json:', e);
envMap = Object.keys(process.env).reduce((a, v) => {
if (v.startsWith('CERC_')) {
a[`process.env.${v}`] = JSON.stringify(process.env[v]);
}
return a;
}, {});
}
console.log('Environment map:', envMap);
EOF
fi
# Add or update webpack configuration
if ! grep -q '__xCfg__' "$NEXT_CONFIG_JS"; then
cat << 'EOF' >> "$NEXT_CONFIG_JS"
const __xCfg__ = (nextConfig) => {
return {
...nextConfig,
webpack: (config, options) => {
config.plugins.push(new webpack.DefinePlugin(envMap));
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options);
}
return config;
},
};
};
EOF
# Update the export/module.exports line
if [ "$IMPORT_OR_REQUIRE" = "import" ]; then
sed -i 's/export default/const __orig_cfg__ =/' "$NEXT_CONFIG_JS"
echo "export default __xCfg__(__orig_cfg__);" >> "$NEXT_CONFIG_JS"
else
sed -i 's/module.exports =/const __orig_cfg__ =/' "$NEXT_CONFIG_JS"
echo "module.exports = __xCfg__(__orig_cfg__);" >> "$NEXT_CONFIG_JS"
fi
fi
# Clean up the config file
js-beautify "$NEXT_CONFIG_JS" > "${NEXT_CONFIG_JS}.pretty"
mv "${NEXT_CONFIG_JS}.pretty" "$NEXT_CONFIG_JS"
# Generate .env-list.json
"${SCRIPT_DIR}/find-env.sh" "$(pwd)" > .env-list.json
# Update package.json
[ ! -f "package.dist" ] && cp package.json package.dist
# Install dependencies
$CERC_BUILD_TOOL install || exit 1
# Get the build command from package.json
BUILD_COMMAND=`jq -r '.scripts.build // ""' package.json`
if [ -z "$BUILD_COMMAND" ]; then
echo "No build command found in package.json. Using default 'next build'."
BUILD_COMMAND="next build"
fi
# Determine the appropriate build commands based on the Next.js version
if semver -p -r ">=14.2.0" "$CERC_NEXT_VERSION"; then
# For >= 14.2.0
CERC_NEXT_COMPILE_COMMAND="${BUILD_COMMAND/next build/next build --experimental-build-mode compile}"
CERC_NEXT_GENERATE_COMMAND="${BUILD_COMMAND/next build/next build --experimental-build-mode generate}"
elif semver -p -r ">=13.4.2" "$CERC_NEXT_VERSION"; then
# For 13.4.2 to 14.1.x
CERC_NEXT_COMPILE_COMMAND="${BUILD_COMMAND/next build/next experimental-compile}"
CERC_NEXT_GENERATE_COMMAND="${BUILD_COMMAND/next build/next experimental-generate}"
else
# For versions before 13.4.2
CERC_NEXT_COMPILE_COMMAND="$BUILD_COMMAND"
CERC_NEXT_GENERATE_COMMAND="$BUILD_COMMAND"
fi
# Update package.json with the appropriate scripts
cat package.json | jq ".scripts.cerc_compile = \"$CERC_NEXT_COMPILE_COMMAND\"" | jq ".scripts.cerc_generate = \"$CERC_NEXT_GENERATE_COMMAND\"" > package.json.$$
mv package.json.$$ package.json
# Run the compile command
time $CERC_BUILD_TOOL run cerc_compile || exit 1
exit 0
@@ -1,31 +0,0 @@
#!/bin/bash
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x
fi
WORK_DIR="${1:-./}"
TMPF=$(mktemp)
cd "$WORK_DIR" || exit 1
for d in $(find . -maxdepth 1 -type d | grep -v '\./\.' | grep '/' | cut -d'/' -f2); do
egrep "/$d[/$]?" .gitignore >/dev/null 2>/dev/null
if [ $? -eq 0 ]; then
continue
fi
for f in $(find "$d" -regex ".*.[tj]sx?$" -type f); do
cat "$f" | tr -s '[:blank:]' '\n' | tr -s '[{},()]' '\n' | egrep -o 'process.env.[A-Za-z0-9_]+' >> $TMPF
done
done
NEXT_CONF="next.config.mjs next.config.js next.config.dist"
for f in $NEXT_CONF; do
if [ -f "$f" ]; then
cat "$f" | tr -s '[:blank:]' '\n' | tr -s '[{},()]' '\n' | egrep -o 'process.env.[A-Za-z0-9_]+' >> $TMPF
fi
done
cat $TMPF | sort -u | jq --raw-input . | jq --slurp .
rm -f $TMPF
@@ -1,65 +0,0 @@
#!/usr/bin/env bash
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x
fi
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
CERC_MAX_GENERATE_TIME=${CERC_MAX_GENERATE_TIME:-120}
tpid=""
ctrl_c() {
kill $tpid $(ps -ef | grep node | grep next | awk '{print $2}') 2>/dev/null
}
trap ctrl_c INT
CERC_BUILD_TOOL="${CERC_BUILD_TOOL}"
if [ -z "$CERC_BUILD_TOOL" ]; then
if [ -f "pnpm-lock.yaml" ]; then
CERC_BUILD_TOOL=pnpm
elif [ -f "yarn.lock" ]; then
CERC_BUILD_TOOL=yarn
elif [ -f "bun.lockb" ]; then
CERC_BUILD_TOOL=bun
else
CERC_BUILD_TOOL=npm
fi
fi
CERC_WEBAPP_FILES_DIR="${CERC_WEBAPP_FILES_DIR:-/app}"
cd "$CERC_WEBAPP_FILES_DIR"
"$SCRIPT_DIR/apply-runtime-env.sh" "`pwd`" .next .next-r
mv .next .next.old
mv .next-r/.next .
if [ "$CERC_NEXTJS_SKIP_GENERATE" != "true" ]; then
jq -e '.scripts.cerc_generate' package.json >/dev/null
if [ $? -eq 0 ]; then
npm run cerc_generate > gen.out 2>&1 &
tail -f gen.out &
tpid=$!
count=0
generate_done="false"
while [ $count -lt $CERC_MAX_GENERATE_TIME ] && [ "$generate_done" == "false" ]; do
sleep 1
count=$((count + 1))
grep 'rendered as static' gen.out > /dev/null
if [ $? -eq 0 ]; then
generate_done="true"
fi
done
if [ $generate_done != "true" ]; then
echo "ERROR: 'npm run cerc_generate' not successful within CERC_MAX_GENERATE_TIME" 1>&2
exit 1
fi
kill $tpid $(ps -ef | grep node | grep next | grep generate | awk '{print $2}') 2>/dev/null
tpid=""
fi
fi
$CERC_BUILD_TOOL start . -- -p ${CERC_LISTEN_PORT:-80}
@@ -4,9 +4,5 @@ source ${CERC_CONTAINER_BASE_DIR}/build-base.sh
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# Two-stage build is to allow us to pick up both the upstream repo's files, and local files here for config
docker build -t cerc/ping-pub-base:local ${build_command_args} -f $SCRIPT_DIR/Dockerfile.base $CERC_REPO_BASE_DIR/cosmos-explorer
if [[ $? -ne 0 ]]; then
echo "FATAL: Base container build failed, exiting"
exit 1
fi
docker build -t cerc/ping-pub-base:local ${build_command_args} -f $SCRIPT_DIR/Dockerfile.base $CERC_REPO_BASE_DIR/explorer
docker build -t cerc/ping-pub:local ${build_command_args} -f $SCRIPT_DIR/Dockerfile $SCRIPT_DIR
@@ -10,7 +10,7 @@ repos:
- git.vdb.to/cerc-io/registry-sdk
- git.vdb.to/cerc-io/laconic-registry-cli
- git.vdb.to/cerc-io/laconic-console
- git.vdb.to/cerc-io/cosmos-explorer
- github.com/ping-pub/explorer
npms:
- registry-sdk
- laconic-registry-cli
@@ -2,28 +2,4 @@
The Package Registry Stack supports a build environment that requires a package registry (initially for NPM packages only).
## Setup
* Setup required repos and build containers:
```bash
laconic-so --stack package-registry setup-repositories
laconic-so --stack package-registry build-containers
```
* Create a deployment:
```bash
laconic-so --stack package-registry deploy init --output package-registry-spec.yml
# Update port mapping in the laconic-loaded.spec file to resolve port conflicts on host if any
laconic-so --stack package-registry deploy create --deployment-dir package-registry-deployment --spec-file package-registry-spec.yml
```
* Start the deployment:
```bash
laconic-so deployment --dir package-registry-deployment start
```
* The local gitea registry can now be accessed at <http://localhost:3000> (the username and password can be taken from the deployment logs)
Setup instructions can be found [here](../build-support/README.md).
@@ -14,7 +14,6 @@
# 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
@@ -261,12 +260,12 @@ class ClusterInfo:
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] = base64.b64encode(open(full_path, 'rb').read()).decode('ASCII')
data[f] = open(full_path, 'rt').read()
spec = client.V1ConfigMap(
metadata=client.V1ObjectMeta(name=f"{self.app_name}-{cfg_map_name}",
labels={"configmap-label": cfg_map_name}),
binary_data=data
data=data
)
result.append(spec)
return result
@@ -21,15 +21,12 @@ import sys
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,
@@ -58,10 +55,7 @@ def process_app_deployment_request(
force_rebuild,
fqdn_policy,
recreate_on_deploy,
webapp_deployer_record,
gpg,
private_key_passphrase,
config_upload_dir,
payment_address,
logger,
):
logger.log("BEGIN - process_app_deployment_request")
@@ -113,31 +107,14 @@ 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 env:
if (
app_deployment_request.attributes.config
and "env" in app_deployment_request.attributes.config
):
env_filename = tempfile.mktemp()
with open(env_filename, "w") as file:
for k, v in env.items():
for k, v in app_deployment_request.attributes.config["env"].items():
file.write("%s=%s\n" % (k, shlex.quote(str(v))))
# 5. determine new or existing deployment
@@ -250,7 +227,7 @@ def process_app_deployment_request(
dns_lrn,
deployment_dir,
app_deployment_request,
webapp_deployer_record,
payment_address,
logger,
)
logger.log("Publication complete.")
@@ -308,12 +285,8 @@ 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", required=True)
@click.option(
"--record-namespace-deployments",
help="eg, lrn://laconic/deployments",
required=True,
)
@click.option("--record-namespace-dns", help="eg, lrn://laconic/dns")
@click.option("--record-namespace-deployments", help="eg, lrn://laconic/deployments")
@click.option(
"--dry-run", help="Don't do anything, just report what would be done.", is_flag=True
)
@@ -340,29 +313,21 @@ def dump_known_requests(filename, requests, status="SEEN"):
)
@click.option(
"--min-required-payment",
help="Requests must have a minimum payment to be processed (in alnt)",
help="Requests must have a minimum payment to be processed",
default=0,
)
@click.option("--lrn", help="The LRN of this deployer.", 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(
"--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,
@@ -385,10 +350,7 @@ def command( # noqa: C901
recreate_on_deploy,
log_dir,
min_required_payment,
lrn,
config_upload_dir,
private_key_file,
private_key_passphrase,
payment_address,
all_requests,
):
if request_id and discover:
@@ -422,18 +384,6 @@ 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:
@@ -442,16 +392,10 @@ def command( # noqa: C901
exclude_tags = [tag.strip() for tag in exclude_tags.split(",") if tag]
laconic = LaconicRegistryClient(laconic_config, log_file=sys.stderr)
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 not payment_address:
payment_address = laconic.whoami().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)
main_logger.log(f"Payment address: {payment_address}")
# Find deployment requests.
# single request
@@ -464,7 +408,7 @@ def command( # noqa: C901
if all_requests:
requests = laconic.app_deployment_requests()
else:
requests = laconic.app_deployment_requests({"deployer": lrn})
requests = laconic.app_deployment_requests({"to": payment_address})
if only_update_state:
if not dry_run:
@@ -543,7 +487,7 @@ def command( # noqa: C901
if all_requests:
deployments = laconic.app_deployments()
else:
deployments = laconic.app_deployments({"deployer": lrn})
deployments = laconic.app_deployments({"by": payment_address})
deployments_by_request = {}
for d in deployments:
if d.attributes.request:
@@ -586,11 +530,7 @@ 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)
@@ -643,10 +583,7 @@ def command( # noqa: C901
force_rebuild,
fqdn_policy,
recreate_on_deploy,
webapp_deployer_record,
gpg,
private_key_passphrase,
config_upload_dir,
payment_address,
build_logger,
)
status = "DEPLOYED"
@@ -667,5 +604,3 @@ def command( # noqa: C901
except Exception as e:
main_logger.log("UNCAUGHT ERROR:" + str(e))
raise e
finally:
shutil.rmtree(tempdir, ignore_errors=True)
@@ -1,91 +0,0 @@
# 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])
@@ -1,175 +0,0 @@
# 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.
import base64
# 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 shutil
import sys
import tempfile
from datetime import datetime
import gnupg
import click
import requests
import yaml
from stack_orchestrator.deploy.webapp.util import (
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
)
@click.option(
"--app",
help="The LRN of the application to deploy.",
required=True,
)
@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.")
@click.option(
"--make-payment",
help="The payment to make (in alnt). The value should be a number or 'auto' to use the deployer's minimum required payment.",
)
@click.option(
"--use-payment", help="The TX id of an existing, unused payment", default=None
)
@click.option("--dns", help="the DNS name to request (default is autogenerated)")
@click.option(
"--dry-run",
help="Don't publish anything, just report what would be done.",
is_flag=True,
)
@click.pass_context
def command(
ctx,
laconic_config,
app,
deployer,
env_file,
config_ref,
make_payment,
use_payment,
dns,
dry_run,
): # noqa: C901
tempdir = tempfile.mkdtemp()
try:
laconic = LaconicRegistryClient(laconic_config)
app_record = laconic.get_record(app)
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}")
if env_file and config_ref:
fatal("Cannot use --env-file and --config-ref at the same time.")
# If env_file
if env_file:
gpg = gnupg.GPG(gnupghome=tempdir)
# 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.")
recip = gpg.list_keys()[0]["uids"][0]
# 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)
@@ -38,7 +38,7 @@ def process_app_removal_request(
deployment_parent_dir,
delete_volumes,
delete_names,
webapp_deployer_record,
payment_address,
):
deployment_record = laconic.get_record(
app_removal_request.attributes.deployment, require=True
@@ -84,7 +84,7 @@ def process_app_removal_request(
"version": "1.0.0",
"request": app_removal_request.id,
"deployment": deployment_record.id,
"deployer": webapp_deployer_record.names[0],
"by": payment_address,
}
}
@@ -168,10 +168,15 @@ def dump_known_requests(filename, requests):
)
@click.option(
"--min-required-payment",
help="Requests must have a minimum payment to be processed (in alnt)",
help="Requests must have a minimum payment to be processed",
default=0,
)
@click.option("--lrn", help="The LRN of this deployer.", 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(
"--all-requests",
help="Handle requests addressed to anyone (by default only requests to"
@@ -193,7 +198,7 @@ def command( # noqa: C901
include_tags,
exclude_tags,
min_required_payment,
lrn,
payment_address,
all_requests,
):
if request_id and discover:
@@ -213,16 +218,8 @@ def command( # noqa: C901
exclude_tags = [tag.strip() for tag in exclude_tags.split(",") if tag]
laconic = LaconicRegistryClient(laconic_config, log_file=sys.stderr)
deployer_record = laconic.get_record(lrn, require=True)
payment_address = 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)
if not payment_address:
payment_address = laconic.whoami().address
# Find deployment removal requests.
# single request
@@ -236,7 +233,7 @@ def command( # noqa: C901
if all_requests:
requests = laconic.app_deployment_removal_requests()
else:
requests = laconic.app_deployment_removal_requests({"deployer": lrn})
requests = laconic.app_deployment_removal_requests({"to": payment_address})
if only_update_state:
if not dry_run:
@@ -315,11 +312,7 @@ 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)
@@ -343,7 +336,7 @@ def command( # noqa: C901
os.path.abspath(deployment_parent_dir),
delete_volumes,
delete_names,
deployer_record,
payment_address,
)
except Exception as e:
main_logger.log(f"ERROR processing removal request {r.id}: {e}")
+6 -26
View File
@@ -1,4 +1,4 @@
# = str(min_required_payment) Copyright © 2023 Vulcanize
# 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
@@ -142,14 +142,14 @@ def confirm_payment(laconic, record, payment_address, min_amount, logger):
# Check if the payment was already used on a
used = laconic.app_deployments(
{"deployer": payment_address, "payment": tx.hash}, all=True
{"by": payment_address, "payment": tx.hash}, all=True
)
if len(used):
logger.log(f"{record.id}: payment {tx.hash} already used on deployment {used}")
return False
used = laconic.app_deployment_removals(
{"deployer": payment_address, "payment": tx.hash}, all=True
{"by": payment_address, "payment": tx.hash}, all=True
)
if len(used):
logger.log(
@@ -453,24 +453,6 @@ class LaconicRegistryClient:
name,
)
def send_tokens(self, address, amount, type="alnt"):
args = [
"laconic",
"-c",
self.config_file,
"registry",
"tokens",
"send",
"--address",
address,
"--quantity",
str(amount),
"--type",
type,
]
return AttrDict(json.loads(logged_cmd(self.log_file, *args)))
def file_hash(filename):
return hashlib.sha1(open(filename).read().encode()).hexdigest()
@@ -481,8 +463,6 @@ def determine_base_container(clone_dir, app_type="webapp"):
raise Exception(f"Unsupported app_type {app_type}")
base_container = "cerc/webapp-base"
if app_type == "webapp/snowball/nextjs":
base_container = "cerc/nextjs-snowball"
if app_type == "webapp/next":
base_container = "cerc/nextjs-base"
elif app_type == "webapp":
@@ -629,7 +609,7 @@ def publish_deployment(
dns_lrn,
deployment_dir,
app_deployment_request=None,
webapp_deployer_record=None,
payment_address=None,
logger=None,
):
if not deploy_record:
@@ -686,8 +666,8 @@ def publish_deployment(
"payment"
] = app_deployment_request.attributes.payment
if webapp_deployer_record:
new_deployment_record["record"]["deployer"] = webapp_deployer_record.names[0]
if payment_address:
new_deployment_record["record"]["by"] = payment_address
if logger:
logger.log("Publishing ApplicationDeploymentRecord.")
+1 -5
View File
@@ -24,9 +24,7 @@ from stack_orchestrator.build import build_webapp
from stack_orchestrator.deploy.webapp import (run_webapp,
deploy_webapp,
deploy_webapp_from_registry,
undeploy_webapp_from_registry,
publish_webapp_deployer,
request_webapp_deployment)
undeploy_webapp_from_registry)
from stack_orchestrator.deploy import deploy
from stack_orchestrator import version
from stack_orchestrator.deploy import deployment
@@ -63,8 +61,6 @@ cli.add_command(run_webapp.command, "run-webapp")
cli.add_command(deploy_webapp.command, "deploy-webapp")
cli.add_command(deploy_webapp_from_registry.command, "deploy-webapp-from-registry")
cli.add_command(undeploy_webapp_from_registry.command, "undeploy-webapp-from-registry")
cli.add_command(publish_webapp_deployer.command, "publish-deployer-to-registry")
cli.add_command(request_webapp_deployment.command, "request-webapp-deployment")
cli.add_command(deploy.command, "deploy") # deploy is an alias for deploy-system
cli.add_command(deploy.command, "deploy-system")
cli.add_command(deployment.command, "deployment")