use serde_json::{json, Value};
const DEFAULT_PRE_LAUNCH_SCRIPT: &str = r#"#!/bin/bash
echo "----------------------------------------------"
echo "Running Phala Cloud Pre-Launch Script v0.0.10"
echo "----------------------------------------------"
set -e
# Function: notify host
notify_host() {
if command -v dstack-util >/dev/null 2>&1; then
dstack-util notify-host -e "$1" -d "$2"
else
tdxctl notify-host -e "$1" -d "$2"
fi
}
notify_host_hoot_info() {
notify_host "boot.progress" "$1"
}
notify_host_hoot_error() {
notify_host "boot.error" "$1"
}
# Function: Perform Docker cleanup
perform_cleanup() {
echo "Pruning unused images"
docker image prune -af
echo "Pruning unused volumes"
docker volume prune -f
notify_host_hoot_info "docker cleanup completed"
}
# Function: Check Docker login status without exposing credentials
check_docker_login() {
# Try to verify login status without exposing credentials
if docker info 2>/dev/null | grep -q "Username"; then
return 0
else
return 1
fi
}
# Main logic starts here
echo "Starting login process..."
# Check if Docker credentials exist
if [[ -n "$DSTACK_DOCKER_USERNAME" && -n "$DSTACK_DOCKER_PASSWORD" ]]; then
echo "Docker credentials found"
# Check if already logged in
if check_docker_login; then
echo "Already logged in to Docker registry"
else
echo "Logging in to Docker registry..."
# Login without exposing password in process list
if [[ -n "$DSTACK_DOCKER_REGISTRY" ]]; then
echo "$DSTACK_DOCKER_PASSWORD" | docker login -u "$DSTACK_DOCKER_USERNAME" --password-stdin "$DSTACK_DOCKER_REGISTRY"
else
echo "$DSTACK_DOCKER_PASSWORD" | docker login -u "$DSTACK_DOCKER_USERNAME" --password-stdin
fi
if [ $? -eq 0 ]; then
echo "Docker login successful"
else
echo "Docker login failed"
notify_host_hoot_error "docker login failed"
exit 1
fi
fi
# Check if AWS ECR credentials exist
elif [[ -n "$DSTACK_AWS_ACCESS_KEY_ID" && -n "$DSTACK_AWS_SECRET_ACCESS_KEY" && -n "$DSTACK_AWS_REGION" && -n "$DSTACK_AWS_ECR_REGISTRY" ]]; then
echo "AWS ECR credentials found"
# Check if AWS CLI is installed
if [ ! -f "./aws/dist/aws" ]; then
notify_host_hoot_info "awscli not installed, installing..."
echo "AWS CLI not installed, installing..."
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64-2.24.14.zip" -o "awscliv2.zip"
echo "6ff031a26df7daebbfa3ccddc9af1450 awscliv2.zip" | md5sum -c
if [ $? -ne 0 ]; then
echo "MD5 checksum failed"
notify_host_hoot_error "awscli install failed"
exit 1
fi
unzip awscliv2.zip &> /dev/null
else
echo "AWS CLI is already installed: ./aws/dist/aws"
fi
# Set AWS credentials as environment variables
export AWS_ACCESS_KEY_ID="$DSTACK_AWS_ACCESS_KEY_ID"
export AWS_SECRET_ACCESS_KEY="$DSTACK_AWS_SECRET_ACCESS_KEY"
export AWS_DEFAULT_REGION="$DSTACK_AWS_REGION"
# Set session token if provided (for temporary credentials)
if [[ -n "$DSTACK_AWS_SESSION_TOKEN" ]]; then
echo "AWS session token found, using temporary credentials"
export AWS_SESSION_TOKEN="$DSTACK_AWS_SESSION_TOKEN"
fi
# Test AWS credentials before attempting ECR login
echo "Testing AWS credentials..."
if ! ./aws/dist/aws sts get-caller-identity &> /dev/null; then
echo "AWS credentials test failed"
# For session token credentials, this might be expected if they're expired
# Log warning but don't fail startup
if [[ -n "$DSTACK_AWS_SESSION_TOKEN" ]]; then
echo "Warning: AWS temporary credentials may have expired, continuing startup"
notify_host_hoot_info "AWS temporary credentials may have expired"
else
echo "AWS credentials test failed"
notify_host_hoot_error "Invalid AWS credentials"
exit 1
fi
else
echo "Logging in to AWS ECR..."
./aws/dist/aws ecr get-login-password --region $DSTACK_AWS_REGION | docker login --username AWS --password-stdin "$DSTACK_AWS_ECR_REGISTRY"
if [ $? -eq 0 ]; then
echo "AWS ECR login successful"
notify_host_hoot_info "AWS ECR login successful"
else
echo "AWS ECR login failed"
# For session token credentials, don't fail startup if login fails
if [[ -n "$DSTACK_AWS_SESSION_TOKEN" ]]; then
echo "Warning: AWS ECR login failed with temporary credentials, continuing startup"
notify_host_hoot_info "AWS ECR login failed with temporary credentials"
else
notify_host_hoot_error "AWS ECR login failed"
exit 1
fi
fi
fi
fi
perform_cleanup
#
# Set root password.
#
if [ -n "$DSTACK_ROOT_PASSWORD" ]; then
echo "$DSTACK_ROOT_PASSWORD" | passwd --stdin root 2>/dev/null || printf '%s
%s
' "$DSTACK_ROOT_PASSWORD" "$DSTACK_ROOT_PASSWORD" | passwd root
unset DSTACK_ROOT_PASSWORD
echo "Root password set/updated from DSTACK_ROOT_PASSWORD"
elif [ -z "$(grep '^root:' /etc/shadow 2>/dev/null | cut -d: -f2)" ]; then
DSTACK_ROOT_PASSWORD=$(
dd if=/dev/urandom bs=32 count=1 2>/dev/null | sha256sum | awk '{print $1}' | cut -c1-32
)
echo "$DSTACK_ROOT_PASSWORD" | passwd --stdin root 2>/dev/null || printf '%s
%s
' "$DSTACK_ROOT_PASSWORD" "$DSTACK_ROOT_PASSWORD" | passwd root
unset DSTACK_ROOT_PASSWORD
echo "Root password set (random auto-init)"
else
echo "Root password already set; no changes."
fi
if [[ -n "$DSTACK_ROOT_PUBLIC_KEY" ]]; then
mkdir -p /home/root/.ssh
echo "$DSTACK_ROOT_PUBLIC_KEY" > /home/root/.ssh/authorized_keys
unset $DSTACK_ROOT_PUBLIC_KEY
echo "Root public key set"
fi
if [[ -n "$DSTACK_AUTHORIZED_KEYS" ]]; then
mkdir -p /home/root/.ssh
echo "$DSTACK_AUTHORIZED_KEYS" > /home/root/.ssh/authorized_keys
unset $DSTACK_AUTHORIZED_KEYS
echo "Root authorized_keys set"
fi
if [[ -S /var/run/dstack.sock ]]; then
export DSTACK_APP_ID=$(curl -s --unix-socket /var/run/dstack.sock http://dstack/Info | jq -j .app_id)
elif [[ -S /var/run/tappd.sock ]]; then
export DSTACK_APP_ID=$(curl -s --unix-socket /var/run/tappd.sock http://dstack/prpc/Tappd.Info | jq -j .app_id)
fi
# Check if DSTACK_GATEWAY_DOMAIN is not set, try to get it from user_config or app-compose.json
# Priority: user_config > app-compose.json
if [[ -z "$DSTACK_GATEWAY_DOMAIN" ]]; then
# First try to get from /dstack/user_config if it exists and is valid JSON
if [[ -f /dstack/user_config ]] && jq empty /dstack/user_config 2>/dev/null; then
if [[ $(jq 'has("default_gateway_domain")' /dstack/user_config 2>/dev/null) == "true" ]]; then
export DSTACK_GATEWAY_DOMAIN=$(jq -j '.default_gateway_domain' /dstack/user_config)
fi
fi
# If still not set, try to get from app-compose.json
if [[ -z "$DSTACK_GATEWAY_DOMAIN" ]] && [[ $(jq 'has("default_gateway_domain")' app-compose.json) == "true" ]]; then
export DSTACK_GATEWAY_DOMAIN=$(jq -j '.default_gateway_domain' app-compose.json)
fi
fi
if [[ -n "$DSTACK_GATEWAY_DOMAIN" ]]; then
export DSTACK_APP_DOMAIN=$DSTACK_APP_ID"."$DSTACK_GATEWAY_DOMAIN
fi
echo "----------------------------------------------"
echo "Script execution completed"
echo "----------------------------------------------"
"#;
pub fn get_default_app_compose() -> Value {
json!({
"allowed_envs": [],
"docker_compose_file": "",
"features": ["kms", "tproxy-net"],
"gateway_enabled": true,
"kms_enabled": true,
"local_key_provider_enabled": false,
"manifest_version": 2,
"name": "",
"no_instance_id": false,
"pre_launch_script": DEFAULT_PRE_LAUNCH_SCRIPT,
"public_logs": true,
"public_sysinfo": true,
"public_tcbinfo": true,
"runner": "docker-compose",
"secure_time": false,
"storage_fs": "zfs",
"tproxy_enabled": true
})
}
pub fn merge_json(base: &mut Value, overlay: &Value) {
match (base, overlay) {
(Value::Object(base_map), Value::Object(overlay_map)) => {
for (key, value) in overlay_map {
base_map.insert(key.clone(), value.clone());
}
}
(base, overlay) => *base = overlay.clone(),
}
}
pub fn merge_with_default_app_compose(user_compose: &Value) -> Value {
let mut default = get_default_app_compose();
merge_json(&mut default, user_compose);
default
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_app_compose_has_required_fields() {
let compose = get_default_app_compose();
assert!(compose.get("allowed_envs").is_some());
assert!(compose.get("docker_compose_file").is_some());
assert!(compose.get("features").is_some());
assert!(compose.get("runner").is_some());
}
#[test]
fn test_merge_json_user_overrides_defaults() {
let mut base = json!({
"allowed_envs": [],
"docker_compose_file": "",
"features": ["kms"]
});
let overlay = json!({
"docker_compose_file": "my-compose.yml",
"allowed_envs": ["AUTH_TOKEN"]
});
merge_json(&mut base, &overlay);
assert_eq!(base["docker_compose_file"], "my-compose.yml");
assert_eq!(base["allowed_envs"], json!(["AUTH_TOKEN"]));
assert_eq!(base["features"], json!(["kms"]));
}
#[test]
fn test_merge_json_adds_new_fields() {
let mut base = json!({
"existing": "value"
});
let overlay = json!({
"new_field": "new_value"
});
merge_json(&mut base, &overlay);
assert_eq!(base["existing"], "value");
assert_eq!(base["new_field"], "new_value");
}
#[test]
fn test_merge_json_non_object_replaces() {
let mut base = json!("string");
let overlay = json!("new_string");
merge_json(&mut base, &overlay);
assert_eq!(base, json!("new_string"));
}
#[test]
fn test_merge_with_default_app_compose() {
let user_compose = json!({
"docker_compose_file": "services:\n app:\n image: test",
"allowed_envs": ["MY_SECRET"]
});
let full = merge_with_default_app_compose(&user_compose);
assert_eq!(full["docker_compose_file"], "services:\n app:\n image: test");
assert_eq!(full["allowed_envs"], json!(["MY_SECRET"]));
assert_eq!(full["runner"], "docker-compose");
assert_eq!(full["manifest_version"], 2);
assert!(full.get("pre_launch_script").is_some());
}
}