Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,34 @@ WeOwn Cloud enables **cohort-based deployment** where each team or organization
**Documentation**: This repository contains complete deployment guides
**Security**: All deployments include enterprise-grade security by default
**Scaling**: Resource optimization across all supported cluster sizes

## 🛠️ **WeOwn CLI (DigitalOcean K8s Deployer)**

The `cli/weown` module provides an interactive interface for deploying and
managing WeOwn stacks on DigitalOcean Kubernetes:

- **Cluster management**: Scale, create, and delete node pools; delete the
entire cluster via `doctl` with confirmation prompts.
- **Infrastructure deployment**: Install shared components such as
`ingress-nginx`, `cert-manager`, ExternalDNS, and the monitoring stack.
- **Application stacks**: Deploy WordPress, Matomo, n8n, AnythingLLM and
other apps into their dedicated namespaces using the curated Helm values.
- **Status & visibility**: List Helm deployments and inspect cluster
resources from a single entry point.

### CLI Setup

1. Ensure `kubectl`, `helm`, and `doctl` are installed and authenticated
against your DigitalOcean account.
2. Create `cli/.env` with at least:
- `DO_TOKEN`, `DO_REGION`, `PROJECT_NAME`, `CLUSTER_NAME`
- `BASE_DOMAIN`, `WP_DOMAIN`, `MATOMO_DOMAIN`, `LETSENCRYPT_EMAIL`
- `WP_ADMIN_PASSWORD`, `MATOMO_DB_ROOT_PASSWORD`, `MATOMO_DB_PASSWORD`
3. From the repository root, run:

```bash
./cli/weown
```

Use the interactive menu to manage node pools, deploy infrastructure and
applications, and verify cluster health for your WeOwn cloud instance.
124 changes: 124 additions & 0 deletions cli/lib/do_k8s.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/bin/bash

source "$(dirname "${BASH_SOURCE[0]}")/styles.sh"

# Config: load env for CLI
# Prefer cli/.env, fall back to project-root .env
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
if [ -f "$BASE_DIR/cli/.env" ]; then
ENV_FILE="$BASE_DIR/cli/.env"
elif [ -f "$BASE_DIR/.env" ]; then
ENV_FILE="$BASE_DIR/.env"
else
ENV_FILE=""
fi

if [ -n "$ENV_FILE" ]; then
export $(grep -v '^#' "$ENV_FILE" | xargs)
fi

check_doctl() {
if ! command -v doctl &> /dev/null; then
log_error "doctl is not installed."
exit 1
fi

# Basic auth check
if ! doctl account get >/dev/null 2>&1; then
log_warn "doctl not authenticated. Trying to use DO_TOKEN..."
if [ -n "$DO_TOKEN" ]; then
if ! doctl auth init -t "$DO_TOKEN" >/dev/null 2>&1; then
log_error "Failed to authenticate doctl with DO_TOKEN."
exit 1
fi
else
log_error "DO_TOKEN not set in .env and doctl not logged in."
exit 1
fi
fi
}

ensure_cluster() {
local cluster_name=${1:-${CLUSTER_NAME:-weown-cluster}}
local region=${2:-${DO_REGION:-nyc3}}
local version="latest"

log_info "Checking for cluster: $cluster_name..."

if doctl kubernetes cluster get "$cluster_name" >/dev/null 2>&1; then
log_success "Cluster '$cluster_name' exists."
else
log_info "Creating cluster '$cluster_name'..."
# Default pool: web-pool
doctl kubernetes cluster create "$cluster_name" \
--region "$region" \
--version "$version" \
--node-pool "name=web-pool;size=s-2vcpu-4gb;count=2" \
--wait
log_success "Cluster created."
fi

log_info "Configuring kubectl context..."
doctl kubernetes cluster kubeconfig save "$cluster_name"
}

list_node_pools() {
local cluster_name=${1:-${CLUSTER_NAME:-weown-cluster}}
doctl kubernetes cluster node-pool list "$cluster_name"
}

scale_node_pool() {
local cluster_name=${1:-${CLUSTER_NAME:-weown-cluster}}
local pool_name=$2
local count=$3

if [ -z "$pool_name" ] || [ -z "$count" ]; then
log_error "Usage: scale_node_pool <pool_name> <count>"
return 1
fi

log_info "Scaling node pool '$pool_name' to $count nodes..."
doctl kubernetes cluster node-pool update "$cluster_name" "$pool_name" --count "$count"
log_success "Scaling initiated."
}

create_node_pool() {
local cluster_name=${1:-${CLUSTER_NAME:-weown-cluster}}
local pool_name=$2
local size=$3
local count=$4
local tags=$5 # Optional setup as "role=value"

log_info "Creating node pool '$pool_name'..."
local cmd="doctl kubernetes cluster node-pool create $cluster_name --name $pool_name --size $size --count $count"
if [ -n "$tags" ]; then
cmd="$cmd --label $tags"
fi
eval $cmd
}

delete_node_pool() {
local cluster_name=${1:-${CLUSTER_NAME:-weown-cluster}}
local pool_name=$2

if [ -z "$pool_name" ]; then
log_error "Usage: delete_node_pool <pool_name|pool_id>"
return 1
fi

log_warn "Deleting node pool '$pool_name' from cluster '$cluster_name' (this removes all its nodes)..."
doctl kubernetes cluster node-pool delete "$cluster_name" "$pool_name" --force
}

delete_cluster() {
local cluster_name=${1:-${CLUSTER_NAME:-weown-cluster}}
log_warn "You are about to DELETE the Kubernetes cluster '$cluster_name'. This will stop all workloads and remove the control plane."
read -p "Type the cluster name ('$cluster_name') to confirm, or anything else to cancel: " confirm
if [ "$confirm" != "$cluster_name" ]; then
log_info "Cluster deletion cancelled."
return 0
fi

log_warn "Deleting cluster '$cluster_name' via doctl..."
doctl kubernetes cluster delete "$cluster_name" --force
}
126 changes: 126 additions & 0 deletions cli/lib/droplet_apps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/bin/bash

# Helpers for deploying applications directly on DigitalOcean Droplets
# (non-Kubernetes), using doctl and DigitalOcean DNS.

source "$(dirname "${BASH_SOURCE[0]}")/styles.sh"
source "$(dirname "${BASH_SOURCE[0]}")/do_k8s.sh"

# Deploy a WordPress One-Click Droplet and wire a domain to it.
# This is intentionally interactive so it can be used from the legacy CLI.

deploy_wordpress_droplet_interactive() {
check_doctl

local default_region=${DO_REGION:-nyc3}
local default_size="s-1vcpu-1gb"

log_info "Deploying WordPress on a DigitalOcean Droplet (non-Kubernetes)."

read -rp "Droplet name [wp-${BASE_DOMAIN:-site}]: " droplet_name
if [ -z "$droplet_name" ]; then
droplet_name="wp-${BASE_DOMAIN:-site}"
fi

read -rp "Region slug [${default_region}]: " region
if [ -z "$region" ]; then
region="$default_region"
fi

read -rp "Size slug [${default_size}]: " size
if [ -z "$size" ]; then
size="$default_size"
fi

read -rp "Domain to map to this droplet (e.g. example.com): " domain
if [ -z "$domain" ]; then
log_error "Domain is required. Aborting."
return 1
fi

log_info "Discovering WordPress image slug from DigitalOcean public images..."
local wp_slug
wp_slug=$(doctl compute image list --public --format Slug --no-header | grep '^wordpress-' | head -n 1)
if [ -z "$wp_slug" ]; then
log_error "Could not find a WordPress image slug via doctl. Please check doctl compute image list."
return 1
fi
log_info "Using WordPress image slug: $wp_slug"

log_info "Creating droplet '$droplet_name' in region '$region' with size '$size'..."
if ! doctl compute droplet create "$droplet_name" \
--region "$region" \
--image "$wp_slug" \
--size "$size" \
--tag-name "wordpress" \
--enable-monitoring \
--wait; then
log_error "Droplet creation failed. Check doctl output above."
return 1
fi

local ip
ip=$(doctl compute droplet list "$droplet_name" --format PublicIPv4 --no-header | tr -d ' ')
if [ -z "$ip" ]; then
log_error "Could not determine droplet IP address."
return 1
fi
log_success "Droplet '$droplet_name' created with IP $ip."

log_info "Ensuring domain '$domain' exists in DigitalOcean DNS..."
if ! doctl compute domain get "$domain" >/dev/null 2>&1; then
if doctl compute domain create "$domain"; then
log_success "Created DNS zone for $domain."
else
log_warn "Failed to create DNS zone for $domain. It may already exist or be managed elsewhere."
fi
fi

log_info "Creating A records (@ and www) pointing to $ip..."
if ! doctl compute domain records create "$domain" \
--record-type A \
--record-name @ \
--record-data "$ip" \
--record-ttl 600; then
log_warn "Failed to create root A record for $domain. It may already exist."
fi

if ! doctl compute domain records create "$domain" \
--record-type A \
--record-name www \
--record-data "$ip" \
--record-ttl 600; then
log_warn "Failed to create www A record for $domain. It may already exist."
fi

log_success "WordPress droplet deployment completed."
echo
echo "You can now visit: http://$domain (or http://$ip) to finish WordPress setup."
echo "Next steps:"
echo " - SSH into the droplet: ssh root@$ip"
echo " - Complete the WordPress installation wizard."
echo " - Optionally run Certbot on the droplet to enable HTTPS for $domain."
}

# Simple sub-menu for droplet-based app deployments

droplet_apps_menu() {
while true; do
echo
log_info "Droplet-based Application Deployment"
echo " 1) Deploy WordPress on a new Droplet"
echo " 2) Back to main menu"
read -rp "Choice: " choice
case "$choice" in
1)
deploy_wordpress_droplet_interactive
;;
2)
return 0
;;
*)
log_warn "Invalid option."
;;
esac
done
}
52 changes: 52 additions & 0 deletions cli/lib/helm_utils.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/bin/bash

source "$(dirname "${BASH_SOURCE[0]}")/styles.sh"

# Helper to find chart path
# Assumes running from ai/cli/
CHART_BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd)"

check_helm() {
if ! command -v helm &> /dev/null; then
log_error "Helm not installed."
exit 1
fi
}

deploy_chart() {
local release_name=$1
local namespace=$2
local chart_path=$3
local values_file=$4

log_info "Deploying '$release_name' to namespace '$namespace'..."

# Create namespace if needed
kubectl create namespace "$namespace" --dry-run=client -o yaml | kubectl apply -f -

local cmd="helm upgrade --install $release_name $chart_path --namespace $namespace"

if [ -f "$values_file" ]; then
cmd="$cmd -f $values_file"
fi

# Execute
if eval $cmd; then
log_success "Deployed $release_name successfully."
else
log_error "Failed to deploy $release_name."
return 1
fi
}

list_deployments() {
helm list -A
}

uninstall_chart() {
local release_name=$1
local namespace=$2

log_warn "Uninstalling '$release_name' from '$namespace'..."
helm uninstall "$release_name" -n "$namespace"
}
Loading