pve_community/ct/opencode.sh

365 lines
10 KiB
Bash
Raw Permalink Normal View History

#!/usr/bin/env bash
set -Eeuo pipefail
BUILD_FUNC_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func"
if command -v curl >/dev/null 2>&1; then
source <(curl -fsSL "$BUILD_FUNC_URL")
elif command -v wget >/dev/null 2>&1; then
source <(wget -qO- "$BUILD_FUNC_URL")
else
printf '[OpenCode] Error: missing required command: curl or wget\n' >&2
exit 1
fi
# Copyright (c) 2026
# License: MIT
APP="OpenCode"
DEFAULT_HOSTNAME="opencode"
if [[ -n "${var_hostname:-}" ]]; then
:
elif [[ -n "${HOSTNAME:-}" && "${HOSTNAME}" != "$(hostname)" ]]; then
var_hostname="$HOSTNAME"
else
var_hostname="$DEFAULT_HOSTNAME"
fi
ONBOOT="${ONBOOT:-1}"
TEMPLATE_ARCH="${TEMPLATE_ARCH:-${ARCH:-amd64}}"
var_tags="${var_tags:-opencode}"
var_ctid="${var_ctid:-${CTID:-}}"
var_cpu="${var_cpu:-${CORES:-2}}"
var_ram="${var_ram:-${MEMORY:-4096}}"
var_disk="${var_disk:-${DISK_GB:-12}}"
var_os="${var_os:-debian}"
var_version="${var_version:-${OSVERSION:-12}}"
var_unprivileged="${var_unprivileged:-${UNPRIVILEGED:-1}}"
var_brg="${var_brg:-${BRIDGE:-vmbr0}}"
var_net="${var_net:-${IP_CONFIG:-dhcp}}"
var_gateway="${var_gateway:-${GATEWAY:-}}"
var_ns="${var_ns:-${NAMESERVER:-}}"
var_searchdomain="${var_searchdomain:-${SEARCHDOMAIN:-}}"
var_template_storage="${var_template_storage:-${TEMPLATE_STORAGE:-}}"
var_container_storage="${var_container_storage:-${CONTAINER_STORAGE:-}}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_SCRIPT="${SCRIPT_DIR%/ct}/install/opencode-install.sh"
OPENCODE_USER="${OPENCODE_USER:-opencode}"
OPENCODE_VERSION="${OPENCODE_VERSION:-}"
OPENCODE_WEB_HOSTNAME="${OPENCODE_WEB_HOSTNAME:-0.0.0.0}"
OPENCODE_WEB_PORT="${OPENCODE_WEB_PORT:-4096}"
OPENCODE_SERVER_USERNAME="${OPENCODE_SERVER_USERNAME:-opencode}"
OPENCODE_SERVER_PASSWORD="${OPENCODE_SERVER_PASSWORD:-}"
SSH_PUBLIC_KEY_FILE="${SSH_PUBLIC_KEY_FILE:-}"
SWAP="${SWAP:-512}"
header_info "$APP"
variables
color
catch_errors
clean_password() {
local raw="${var_pw:-}"
[[ -n "$raw" ]] || return 0
case "$raw" in
--password\ *) raw="${raw#--password }" ;;
-password\ *) raw="${raw#-password }" ;;
esac
while [[ "$raw" == -* ]]; do
raw="${raw#-}"
done
[[ -n "$raw" ]] && printf '%s\n' "$raw"
}
resolve_template_name() {
pveam available --section system | awk -v os="$var_os" -v ver="$var_version" -v arch="$TEMPLATE_ARCH" '
$2 ~ ("^" os "-" ver "-standard_.*_" arch "\\.tar\\.(gz|xz|zst)$") {print $2; exit}
'
}
build_net_arg() {
local net="name=eth0,bridge=${BRG:-vmbr0},ip=${NET:-dhcp}"
[[ -n "${GATE:-}" ]] && net+=",gw=${GATE}"
[[ -n "${VLAN:-}" ]] && net+=",tag=${VLAN}"
[[ -n "${MTU:-}" ]] && net+=",mtu=${MTU}"
[[ -n "${MAC:-}" ]] && net+=",hwaddr=${MAC}"
case "${IPV6_METHOD:-none}" in
auto) net+=",ip6=auto" ;;
dhcp) net+=",ip6=dhcp" ;;
static)
[[ -n "${IPV6_STATIC:-}" ]] && net+=",ip6=${IPV6_STATIC}"
;;
esac
printf '%s\n' "$net"
}
build_features_arg() {
if [[ -n "${FEATURES:-}" ]]; then
printf '%s\n' "$FEATURES"
return 0
fi
local features=()
if [[ "${ENABLE_NESTING:-1}" == "1" ]]; then
features+=("nesting=1")
fi
if [[ "${CT_TYPE:-1}" == "1" ]]; then
features+=("keyctl=1")
fi
if [[ "${ENABLE_FUSE:-no}" == "yes" ]]; then
features+=("fuse=1")
fi
if [[ "${ENABLE_MKNOD:-0}" == "1" ]]; then
features+=("mknod=1")
fi
if [[ -n "${var_mount_fs:-}" ]]; then
features+=("mount=${var_mount_fs}")
fi
if [[ ${#features[@]} -gt 0 ]]; then
local joined="${features[*]}"
printf '%s\n' "${joined// /,}"
fi
}
install_custom_ssh_keys() {
[[ -n "$SSH_PUBLIC_KEY_FILE" ]] || return 0
[[ -f "$SSH_PUBLIC_KEY_FILE" ]] || {
msg_error "SSH key file not found: $SSH_PUBLIC_KEY_FILE"
exit 116
}
msg_info "Installing custom SSH public keys"
pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh'
pct push "$CTID" "$SSH_PUBLIC_KEY_FILE" /tmp/opencode-authorized_keys --perms 600 >/dev/null
pct exec "$CTID" -- sh -c 'cat /tmp/opencode-authorized_keys >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys && rm -f /tmp/opencode-authorized_keys'
msg_ok "Installed custom SSH public keys"
}
resolve_opencode_user() {
if id -u "$OPENCODE_USER" >/dev/null 2>&1; then
printf '%s\n' "$OPENCODE_USER"
return 0
fi
if id -u opencode >/dev/null 2>&1; then
printf '%s\n' "opencode"
return 0
fi
local uid_1000_user
uid_1000_user="$(getent passwd 1000 | cut -d: -f1 || true)"
[[ -n "$uid_1000_user" ]] && printf '%s\n' "$uid_1000_user"
}
update_script() {
header_info
check_container_storage
check_container_resources
msg_info "Updating base system"
$STD apt-get update
$STD apt-get upgrade -y
msg_ok "Base system updated"
local install_user
install_user="$(resolve_opencode_user)"
if [[ -z "$install_user" ]]; then
msg_warn "No OpenCode user found, skipping OpenCode update"
msg_ok "Updated successfully!"
exit
fi
msg_info "Updating OpenCode"
if [[ -n "$OPENCODE_VERSION" ]]; then
$STD su - "$install_user" -c "curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path --version ${OPENCODE_VERSION}"
else
$STD su - "$install_user" -c "curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path"
fi
if [[ -x "/home/${install_user}/.opencode/bin/opencode" ]]; then
ln -sf "/home/${install_user}/.opencode/bin/opencode" /usr/local/bin/opencode
fi
install -d -m 0755 -o "$install_user" -g "$install_user" /workspace
if systemctl list-unit-files opencode-web.service >/dev/null 2>&1; then
$STD systemctl daemon-reload
$STD systemctl restart opencode-web.service
fi
msg_ok "OpenCode updated"
msg_ok "Updated successfully!"
exit
}
build_container() {
[[ -f "$INSTALL_SCRIPT" ]] || {
msg_error "Missing installer script at $INSTALL_SCRIPT"
exit 117
}
export CTID="$CT_ID"
local template_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
local container_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"
local template_name
local template_ref
local net_arg
local features_arg
local clean_pw
local tags
local version
local install_path="/root/opencode-install.sh"
local -a pct_args
[[ -n "$template_storage" ]] || template_storage="$(pvesm status --content vztmpl | awk 'NR>1 {print $1; exit}')"
[[ -n "$container_storage" ]] || container_storage="$(pvesm status --content rootdir | awk 'NR>1 {print $1; exit}')"
[[ -n "$template_storage" ]] || {
msg_error "No storage found for content type 'vztmpl'"
exit 118
}
[[ -n "$container_storage" ]] || {
msg_error "No storage found for content type 'rootdir'"
exit 119
}
msg_info "Updating template list"
pveam update >/dev/null
template_name="$(resolve_template_name)"
[[ -n "$template_name" ]] || {
msg_error "No template found for ${var_os}-${var_version} (amd64)"
exit 120
}
msg_info "Ensuring template ${template_name} exists on ${template_storage}"
if ! pveam list "$template_storage" | awk 'NR>1 {print $2}' | grep -Fxq "$template_name"; then
pveam download "$template_storage" "$template_name"
fi
template_ref="${template_storage}:vztmpl/${template_name}"
net_arg="$(build_net_arg)"
features_arg="$(build_features_arg)"
clean_pw="$(clean_password || true)"
tags="community-script;${var_tags}"
pct_args=(
create "$CTID" "$template_ref"
--hostname "$HN"
--ostype "$var_os"
--unprivileged "$CT_TYPE"
--cores "$CORE_COUNT"
--memory "$RAM_SIZE"
--swap "$SWAP"
--rootfs "${container_storage}:${DISK_SIZE}"
--net0 "$net_arg"
--onboot "$ONBOOT"
--tags "$tags"
)
if [[ -n "$features_arg" ]]; then
pct_args+=(--features "$features_arg")
fi
if [[ -n "${var_ns:-}" ]]; then
pct_args+=(--nameserver "${var_ns}")
fi
if [[ -n "${var_searchdomain:-}" ]]; then
pct_args+=(--searchdomain "${var_searchdomain}")
fi
if [[ -n "${CT_TIMEZONE:-}" ]]; then
pct_args+=(--timezone "${CT_TIMEZONE}")
fi
if [[ -n "$clean_pw" ]]; then
pct_args+=(--password "$clean_pw")
fi
msg_info "Creating LXC Container"
pct "${pct_args[@]}"
msg_ok "Created LXC Container"
if [[ "${PROTECT_CT:-no}" == "yes" || "${PROTECT_CT:-0}" == "1" ]]; then
pct set "$CTID" --protection 1 >/dev/null
fi
msg_info "Starting LXC Container"
pct start "$CTID"
sleep 5
msg_ok "Started LXC Container"
install_ssh_keys_into_ct || true
install_custom_ssh_keys
msg_info "Pushing OpenCode installer"
pct push "$CTID" "$INSTALL_SCRIPT" "$install_path" --perms 755
msg_ok "Pushed OpenCode installer"
msg_info "Installing OpenCode"
pct exec "$CTID" -- env \
OPENCODE_USER="$OPENCODE_USER" \
OPENCODE_VERSION="$OPENCODE_VERSION" \
OPENCODE_WEB_HOSTNAME="$OPENCODE_WEB_HOSTNAME" \
OPENCODE_WEB_PORT="$OPENCODE_WEB_PORT" \
OPENCODE_SERVER_USERNAME="$OPENCODE_SERVER_USERNAME" \
OPENCODE_SERVER_PASSWORD="$OPENCODE_SERVER_PASSWORD" \
bash "$install_path"
msg_ok "Installed OpenCode"
version="$(pct exec "$CTID" -- su - "$OPENCODE_USER" -c 'opencode --version' 2>/dev/null | tail -n 1 || true)"
[[ -n "$version" ]] && export OPENCODE_INSTALLED_VERSION="$version"
}
description() {
local version="${OPENCODE_INSTALLED_VERSION:-}"
local ct_ip
local description
ct_ip="$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}' || true)"
description=$(
cat <<EOF
<div align='center'>
<a href='https://github.com/anomalyco/opencode' target='_blank' rel='noopener noreferrer'>
<h2 style='font-size: 24px; margin: 20px 0;'>${APP} LXC</h2>
</a>
<p style='margin: 12px 0;'>OpenCode web-enabled development container.</p>
<p style='margin: 12px 0;'>
<a href='https://github.com/anomalyco/opencode' target='_blank' rel='noopener noreferrer'>GitHub</a>
&nbsp;|&nbsp;
<a href='https://opencode.ai/docs/web/' target='_blank' rel='noopener noreferrer'>Web Docs</a>
</p>
<p style='margin: 12px 0;'>
Web UI: ${ct_ip:-Unavailable}:${OPENCODE_WEB_PORT}
</p>
</div>
EOF
)
pct set "$CTID" --description "$description" >/dev/null 2>&1 || true
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
if [[ -n "$version" ]]; then
echo -e "${INFO}${YW} Installed version: ${BGN}${version}${CL}"
fi
echo -e "${INFO}${YW} Next steps:${CL}"
echo -e "${TAB}Web UI: http://${ct_ip:-<ct-ip>}:${OPENCODE_WEB_PORT}"
echo -e "${TAB}pct enter ${CTID}"
echo -e "${TAB}su - ${OPENCODE_USER}"
echo -e "${TAB}opencode"
echo -e "${INFO}${YW} Configure a provider with /connect or provider environment variables.${CL}"
}
start
build_container
description