This commit is contained in:
2025-09-29 14:14:30 +00:00
parent 953a8d8825
commit 9701a6ed89
28 changed files with 2058 additions and 377 deletions

View File

@@ -1,6 +1,21 @@
# Start from the universal devcontainer base image
FROM mcr.microsoft.com/devcontainers/universal:2-linux
ARG LAZYGIT_VERSION=0.55.1
ARG LAZYGIT_SHA256=6385a699dde302b7fdcd1cc8910ae225ed0c19a230285569c586051576f0d6a3
ARG LAZYDOCKER_VERSION=0.24.1
ARG LAZYDOCKER_SHA256=461cacf618e1020dff1d7896248c1c1f2267d5c25fb529755e4b9c43c5d1d4a5
ARG SUPERFILE_VERSION=1.3.3
ARG SUPERFILE_SHA256=b74dffa446bdbeaef38cae0815e1714f78d5bffc0b39aafd1bd9f26ef191210a
ARG BTOP_VERSION=1.4.5
ARG BTOP_SHA256=206b0f9334e93c06de9025eaf90676c374ca79815b41dadff1b36ef4e4e6d1d4
ARG CODE_SERVER_VERSION=4.104.2
ARG CODE_SERVER_SHA256=bc650b57fd8d0bcee952c97308dd43ae37ad8dc11b83a713d8eca8ce823fefd9
ARG UV_VERSION=0.8.22
ARG UV_PYTHON_VERSION=3.12.5
ARG GO_VERSION=1.25.1
ARG GO_SHA256=7716a0d940a0f6ae8e1f3b3f4f36299dc53e31b16840dbd171254312c41ca12e
# Switch to root for installations
USER root
@@ -25,14 +40,15 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && \
curl \
wget \
unzip \
xz-utils \
# Development tools
jq \
httpie \
software-properties-common \
# Build tools
build-essential \
cmake \
pkg-config \
make \
# Database clients
postgresql-client \
redis-tools \
@@ -43,11 +59,90 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && \
git \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Install the Go toolchain from upstream tarball to guarantee a fresh version
RUN set -eux; \
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tar.gz; \
echo "${GO_SHA256} /tmp/go.tar.gz" | sha256sum -c -; \
rm -rf /usr/local/go; \
tar -C /usr/local -xzf /tmp/go.tar.gz; \
rm -f /tmp/go.tar.gz
# Install Python 3.12 system-wide and set as default
RUN set -eux; \
add-apt-repository ppa:deadsnakes/ppa -y; \
apt-get update; \
apt-get install -y python3.12 python3.12-venv python3.12-dev; \
python3.12 -m ensurepip --upgrade; \
python3.12 -m pip install --upgrade pip; \
update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 2; \
update-alternatives --set python3 /usr/bin/python3.12; \
update-alternatives --install /usr/bin/python python /usr/bin/python3.12 2; \
update-alternatives --set python /usr/bin/python3.12; \
if command -v pip3.12 >/dev/null 2>&1; then \
ln -sf "$(command -v pip3.12)" /usr/local/bin/pip3; \
ln -sf "$(command -v pip3.12)" /usr/local/bin/pip; \
fi; \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Install pinned CLI utilities that workspaces depend on
RUN set -eux; \
tmpdir=$(mktemp -d); \
cd "$tmpdir"; \
curl -fsSL "https://github.com/yorukot/superfile/releases/download/v${SUPERFILE_VERSION}/superfile-linux-v${SUPERFILE_VERSION}-amd64.tar.gz" -o superfile.tar.gz; \
echo "${SUPERFILE_SHA256} superfile.tar.gz" | sha256sum -c -; \
tar -xzf superfile.tar.gz; \
install -Dm755 "dist/superfile-linux-v${SUPERFILE_VERSION}-amd64/spf" /usr/local/bin/spf; \
curl -fsSL "https://github.com/jesseduffield/lazygit/releases/download/v${LAZYGIT_VERSION}/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz" -o lazygit.tar.gz; \
echo "${LAZYGIT_SHA256} lazygit.tar.gz" | sha256sum -c -; \
tar -xzf lazygit.tar.gz lazygit; \
install -Dm755 lazygit /usr/local/bin/lazygit; \
curl -fsSL "https://github.com/jesseduffield/lazydocker/releases/download/v${LAZYDOCKER_VERSION}/lazydocker_${LAZYDOCKER_VERSION}_Linux_x86_64.tar.gz" -o lazydocker.tar.gz; \
echo "${LAZYDOCKER_SHA256} lazydocker.tar.gz" | sha256sum -c -; \
tar -xzf lazydocker.tar.gz lazydocker; \
install -Dm755 lazydocker /usr/local/bin/lazydocker; \
curl -fsSL "https://github.com/aristocratos/btop/releases/download/v${BTOP_VERSION}/btop-x86_64-linux-musl.tbz" -o btop.tbz; \
echo "${BTOP_SHA256} btop.tbz" | sha256sum -c -; \
tar -xjf btop.tbz; \
install -Dm755 btop/bin/btop /usr/local/bin/btop; \
cd /; \
rm -rf "$tmpdir"
# Install code-server into a fixed prefix so the module can run in offline mode
RUN set -eux; \
curl -fsSL "https://github.com/coder/code-server/releases/download/v${CODE_SERVER_VERSION}/code-server-${CODE_SERVER_VERSION}-linux-amd64.tar.gz" -o /tmp/code-server.tar.gz; \
echo "${CODE_SERVER_SHA256} /tmp/code-server.tar.gz" | sha256sum -c -; \
rm -rf /opt/code-server; \
mkdir -p /opt; \
tar -xzf /tmp/code-server.tar.gz -C /opt; \
mv /opt/code-server-${CODE_SERVER_VERSION}-linux-amd64 /opt/code-server; \
ln -sf /opt/code-server/bin/code-server /usr/local/bin/code-server; \
rm -f /tmp/code-server.tar.gz; \
chown -R coder:coder /opt/code-server
# Install the uv Python toolchain manager system-wide
RUN set -eux; \
curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin UV_VERSION=${UV_VERSION} sh
# Preinstall JavaScript tooling for all users
RUN set -eux; \
npm install -g --silent \
pnpm@latest \
yarn@latest \
turbo@latest \
@anthropic-ai/claude-code@latest \
vercel@latest \
netlify-cli@latest \
tsx@latest \
nodemon@latest \
tldr@latest \
fkill-cli@latest \
repomix@latest
# Switch to coder user for user-specific installations
USER coder
# Install uv early for Python package management
RUN pip install --user uv
# Ensure user-local bin directories and system installs are on PATH
ENV PATH="/home/coder/.venv/bin:/usr/local/go/bin:/usr/local/bin:/home/coder/.local/bin:/home/coder/bin:${PATH}"
# Install Rust-based tools using cargo (check if available first)
RUN if [ -f ~/.cargo/env ]; then \
@@ -58,34 +153,78 @@ RUN if [ -f ~/.cargo/env ]; then \
echo "Rust/cargo not available, skipping Rust tools"; \
fi
# Install Node.js and Python packages in single layer
RUN echo "Installing Node.js packages..." && \
npm install -g --silent \
pnpm@latest \
yarn@latest \
turbo@latest \
@anthropic-ai/claude-code@latest \
vercel@latest \
netlify-cli@latest \
tsx@latest \
nodemon@latest \
tldr@latest \
fkill-cli@latest \
repomix@latest && \
echo "Installing Python packages..." && \
export PATH="/home/coder/.local/bin:$PATH" && \
~/.local/bin/uv tool install poetry && \
~/.local/bin/uv tool install black && \
~/.local/bin/uv tool install ruff && \
~/.local/bin/uv tool install mypy && \
~/.local/bin/uv tool install pytest && \
# Install Python tooling for coder user
RUN echo "Installing Python packages..." && \
uv tool install poetry && \
uv tool install black && \
uv tool install ruff && \
uv tool install mypy && \
uv tool install pytest && \
pip install --user --quiet pipenv httpx rich && \
echo "All packages installed successfully"
echo "Python packages installed successfully"
# Create necessary directories and copy scripts
# Preinstall Python 3.12 virtual environment and Marimo environment expected by the template
RUN set -eux; \
uv python install ${UV_PYTHON_VERSION}; \
uv venv --python ${UV_PYTHON_VERSION} /home/coder/.venv; \
/home/coder/.venv/bin/python -m ensurepip --upgrade; \
/home/coder/.venv/bin/python -m pip install --upgrade pip; \
/home/coder/.venv/bin/python -m pip install --upgrade marimo; \
mkdir -p /home/coder/workspaces/notebooks; \
cat <<MARIMO_APP > /home/coder/workspaces/notebooks/welcome.py
import marimo
__generated_with = "0.16.0"
app = marimo.App()
@app.cell
def __():
import marimo as mo
return mo,
@app.cell
def __(mo):
mo.md("# Welcome to Marimo!")
return
@app.cell
def __(mo):
mo.md("This is your interactive notebook environment.")
return
if __name__ == "__main__":
app.run()
MARIMO_APP
# Seed code-server configuration matching the Terraform defaults
RUN set -eux; \
mkdir -p /home/coder/.config/code-server; \
cat <<CONFIG > /home/coder/.config/code-server/config.yaml
bind-addr: 127.0.0.1:13337
auth: none
cert: false
CONFIG
# Create necessary directories and stage workspace helper scripts (if present)
USER root
COPY tf/scripts/*.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/*.sh
COPY . /tmp/devcontainer-src
RUN if [ -d /tmp/devcontainer-src/terraform/scripts ]; then \
cp -r /tmp/devcontainer-src/terraform/scripts/*.sh /usr/local/bin/; \
if [ -d /tmp/devcontainer-src/terraform/scripts/agentapi ]; then \
rm -rf /usr/local/bin/agentapi; \
cp -r /tmp/devcontainer-src/terraform/scripts/agentapi /usr/local/bin/agentapi; \
fi; \
elif [ -d /tmp/devcontainer-src/scripts ]; then \
cp -r /tmp/devcontainer-src/scripts/*.sh /usr/local/bin/; \
if [ -d /tmp/devcontainer-src/scripts/agentapi ]; then \
rm -rf /usr/local/bin/agentapi; \
cp -r /tmp/devcontainer-src/scripts/agentapi /usr/local/bin/agentapi; \
fi; \
else \
echo "No workspace helper scripts found; skipping copy"; \
fi && \
find /usr/local/bin -maxdepth 1 -type f -name *.sh -exec chmod +x {} \; && \
rm -rf /tmp/devcontainer-src
# Switch to coder user
USER coder
@@ -101,9 +240,17 @@ RUN echo 'export PATH="$HOME/.local/bin:$HOME/bin:$PATH"' >> ~/.zshrc && \
echo 'command -v starship >/dev/null 2>&1 && eval "$(starship init zsh)"' >> ~/.zshrc && \
echo 'command -v zoxide >/dev/null 2>&1 && eval "$(zoxide init zsh)"' >> ~/.zshrc
# Set up IDE configurations using our scripts
RUN /usr/local/bin/cursor-setup.sh && \
/usr/local/bin/windsurf-setup.sh
# Set up IDE configurations if the helper scripts are present
RUN if [ -f /usr/local/bin/cursor-setup.sh ]; then \
/usr/local/bin/cursor-setup.sh; \
else \
echo "cursor-setup.sh not found; skipping"; \
fi && \
if [ -f /usr/local/bin/windsurf-setup.sh ]; then \
/usr/local/bin/windsurf-setup.sh; \
else \
echo "windsurf-setup.sh not found; skipping"; \
fi
# Create devinfo utility
RUN cat <<'EOF' > "$HOME/bin/devinfo" \
@@ -169,4 +316,4 @@ ENV META_DIR=/tmp/git-metadata
EXPOSE 3000 5000 8000 8080 8888 5050 6333
# Default command
CMD ["/bin/zsh"]
CMD ["/bin/zsh"]

View File

@@ -1,2 +1,3 @@
# code-tools
cloudflared.exe service install eyJhIjoiZjIxNTFlNzZiYzQ3NmZkMWRhNWMzNmRkMDBkZWIzYzEiLCJ0IjoiMDU3ODVmZTQtMGRlOS00OTQ1LThhMWMtOTMzZWRiYmQzOTc0IiwicyI6IlptRXpOekUyTWpJdE5EaGlOaTAwTmpVeExUaGxNall0Tnpjd01HRTJNVEJqTkRsaSJ9

270
cert-mgmt/README.md Normal file
View File

@@ -0,0 +1,270 @@
# TLS Certificate Manager for git.lab
A comprehensive solution for generating, deploying, and managing TLS certificates for git.lab across your network infrastructure, including SSH-accessible hosts and Docker containers.
## 🎯 Overview
This tool provides:
- **Certificate Generation**: Creates self-signed TLS certificates for git.lab domain
- **Network Discovery**: Automatically scans for SSH-accessible hosts and Docker containers
- **Automated Deployment**: Deploys certificates to hosts and containers in parallel
- **Verification**: Validates certificate installation and functionality
- **Coolify Integration**: Works with existing Coolify proxy infrastructure
## 📋 Prerequisites
### Required Tools
- `openssl` - For certificate operations
- `ssh`/`scp` - For remote host access
- `docker` - For container operations
- `curl` - For HTTPS testing
- `nslookup` - For DNS resolution
### Network Requirements
- SSH key-based authentication to target hosts
- Docker daemon access
- DNS resolution for git.lab domain
### Permissions
- Sudo access for certificate installation
- Docker socket access
- Read/write access to certificate directories
## 🚀 Quick Start
### 1. Initial Setup
```bash
# Clone or create the directory structure
mkdir -p ~/tls-cert-manager/{certificates,scripts,inventory,logs}
cd ~/tls-cert-manager
# Make scripts executable
chmod +x scripts/*.sh
```
### 2. Generate Certificate
```bash
./scripts/generate_certificate.sh
```
### 3. Create Network Inventory
```bash
# This scans your network - may take a few minutes
./scripts/create_inventory.sh
```
### 4. Deploy Certificates
```bash
# Full deployment (recommended)
./scripts/cert_deployment_orchestrator.sh full-deploy
# Or deploy selectively
./scripts/cert_deployment_orchestrator.sh deploy-hosts
./scripts/cert_deployment_orchestrator.sh deploy-containers
```
### 5. Verify Installation
```bash
./scripts/cert_deployment_orchestrator.sh validate
```
## 📁 Directory Structure
```
tls-cert-manager/
├── certificates/ # Generated certificates
│ ├── git.lab.crt # Public certificate
│ └── git.lab.key # Private key (600 permissions)
├── scripts/ # Deployment scripts
│ ├── generate_certificate.sh
│ ├── create_inventory.sh
│ ├── deploy_to_hosts.sh
│ ├── deploy_to_containers.sh
│ └── cert_deployment_orchestrator.sh
├── inventory/ # Network inventory
│ └── network_inventory.yaml
├── logs/ # Deployment logs
│ └── deployment.log
└── README.md # This file
```
## 🔧 Script Details
### Certificate Generation (`generate_certificate.sh`)
- Creates 4096-bit RSA certificate valid for 1 year
- Includes SAN entries for `git.lab`, `*.git.lab`, and IP `192.168.50.210`
- Sets proper file permissions (644 for .crt, 600 for .key)
- Optionally copies to Coolify directory if available
### Network Discovery (`create_inventory.sh`)
- Scans 192.168.50.x subnet for SSH-accessible hosts
- Enumerates running Docker containers
- Excludes Coolify-managed containers
- Creates YAML inventory file
### Host Deployment (`deploy_to_hosts.sh`)
- Connects via SSH to each target host
- Installs certificate in `/usr/local/share/ca-certificates/`
- Runs `update-ca-certificates`
- Deploys SSL certificates to `/etc/ssl/git.lab/`
- Gracefully reloads web services (nginx, apache2, httpd)
### Container Deployment (`deploy_to_containers.sh`)
- Iterates through Docker containers
- Skips Coolify-managed containers automatically
- Installs ca-certificates package if needed
- Updates certificate store within containers
- Handles different Linux distributions (apt, yum, apk)
### Orchestrator (`cert_deployment_orchestrator.sh`)
- Unified interface for all operations
- Parallel deployment for efficiency
- Comprehensive validation and verification
- Colored output and logging
- Error handling and retry logic
## 🔍 Validation Report
The current setup validation shows:
### ✅ What's Working
- **Certificate Files**: Properly generated with correct permissions
- **Certificate Validity**: Valid for 365 days, proper SAN entries
- **Scripts**: All executable and ready to use
- **Network**: git.lab resolves correctly, HTTPS service responding
- **Docker**: 96+ containers available for deployment
### ⚠️ Current Configuration
- **Active Certificate**: Coolify is serving `*.lab` wildcard certificate (valid until 2035)
- **New Certificate**: Our `git.lab` specific certificate ready for deployment
- **Scope**: New certificate covers `git.lab`, `*.git.lab`, and IP `192.168.50.210`
## 📖 Usage Examples
### Basic Operations
```bash
# Check current status
./scripts/cert_deployment_orchestrator.sh status
# Validate setup
./scripts/cert_deployment_orchestrator.sh validate
# Generate new certificate
./scripts/cert_deployment_orchestrator.sh generate
```
### Deployment Options
```bash
# Deploy to everything
./scripts/cert_deployment_orchestrator.sh full-deploy
# Deploy to hosts only
./scripts/cert_deployment_orchestrator.sh deploy-hosts
# Deploy to containers only
./scripts/cert_deployment_orchestrator.sh deploy-containers
# Skip specific targets
./scripts/cert_deployment_orchestrator.sh full-deploy --skip-containers
```
### Troubleshooting
```bash
# Check logs
tail -f logs/deployment.log
# Test individual scripts
./scripts/deploy_to_hosts.sh
./scripts/deploy_to_containers.sh
# Manual certificate verification
openssl x509 -in certificates/git.lab.crt -text -noout
```
## 🛡️ Security Considerations
### Certificate Security
- Private keys have 600 permissions
- Certificates use 4096-bit RSA encryption
- Self-signed certificates for internal use only
- Regular rotation recommended (currently valid for 1 year)
### Network Security
- SSH key-based authentication required
- Temporary files cleaned up after deployment
- Sudo access required only for certificate installation
- Coolify-managed containers are automatically excluded
### Access Control
- Scripts validate prerequisites before execution
- Error handling prevents partial deployments
- Comprehensive logging for audit trails
## 🔄 Certificate Rotation
To rotate certificates:
1. **Generate new certificate**:
```bash
./scripts/generate_certificate.sh
```
2. **Deploy to all targets**:
```bash
./scripts/cert_deployment_orchestrator.sh full-deploy
```
3. **Verify deployment**:
```bash
./scripts/cert_deployment_orchestrator.sh verify
```
## 🐛 Troubleshooting
### Common Issues
**SSH Connection Failures**
- Ensure SSH key authentication is configured
- Check network connectivity to target hosts
- Verify sudo access on target hosts
**Docker Permission Issues**
- Add user to docker group: `sudo usermod -aG docker $USER`
- Ensure Docker daemon is running
- Check container accessibility
**Certificate Validation Errors**
- Verify certificate files exist and have correct permissions
- Check certificate expiration dates
- Ensure OpenSSL is available
**Coolify Integration Issues**
- Check if `/data/coolify/proxy/certificates/` exists
- Verify proper permissions on Coolify directories
- Consider manual certificate placement if needed
### Log Analysis
Check deployment logs for detailed error information:
```bash
tail -f logs/deployment.log
grep ERROR logs/deployment.log
```
## 🤝 Contributing
To extend or modify the certificate manager:
1. **Add new target types**: Modify inventory creation and deployment scripts
2. **Customize certificate parameters**: Edit `generate_certificate.sh`
3. **Add verification methods**: Extend validation functions
4. **Implement new deployment strategies**: Create additional deployment scripts
## 📝 License
This tool is provided as-is for internal infrastructure management. Ensure compliance with your organization's security policies before deployment.
---
**Last Updated**: September 29, 2025
**Version**: 1.0
**Supported OS**: Ubuntu Linux (tested on Ubuntu with Zsh)

View File

@@ -0,0 +1,13 @@
# TLS Certificate Deployment Inventory
# Generated: $(date -Iseconds)
network_info:
primary_ip: "192.168.50.210"
subnets:
- "192.168.50.0/24"
- "10.0.32.0/24"
- "10.0.43.0/24"
- "10.0.15.0/24"
# SSH-accessible hosts (exclude current host)
hosts:

View File

@@ -0,0 +1,252 @@
#!/bin/zsh
# TLS Certificate Deployment Orchestrator
# Unified script for certificate deployment and verification
set -e
CERT_FILE="certificates/git.lab.crt"
KEY_FILE="certificates/git.lab.key"
DOMAIN="git.lab"
INVENTORY_FILE="inventory/network_inventory.yaml"
LOG_FILE="logs/deployment.log"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
function log() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local message="[$timestamp] $1"
echo -e "$message"
mkdir -p logs
echo "$message" >> "$LOG_FILE" 2>/dev/null || true
}
function log_success() {
log "${GREEN}SUCCESS:${NC} $1"
}
function log_error() {
log "${RED}ERROR:${NC} $1"
}
function log_warning() {
log "${YELLOW}WARNING:${NC} $1"
}
function log_info() {
log "${BLUE}INFO:${NC} $1"
}
function check_prerequisites() {
log_info "Checking prerequisites..."
local missing_deps=()
# Check for required tools
for tool in openssl ssh scp docker; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing_deps+=("$tool")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
log_error "Missing required tools: ${missing_deps[*]}"
exit 1
fi
# Check for certificate files
if [[ ! -f "$CERT_FILE" ]]; then
log_error "Certificate file not found: $CERT_FILE"
log_info "Run: ./scripts/generate_certificate.sh"
exit 1
fi
# Check for inventory file
if [[ ! -f "$INVENTORY_FILE" ]]; then
log_warning "Inventory file not found. Creating one..."
./scripts/create_inventory.sh
fi
log_success "Prerequisites check passed"
}
function verify_certificate_installation() {
log_info "Verifying certificate installation..."
# Check local certificate validity
if openssl x509 -in "$CERT_FILE" -checkend 86400 >/dev/null 2>&1; then
log_success "Local certificate is valid"
else
log_error "Local certificate is expired or invalid"
return 1
fi
# Check if current certificate is already being served
if curl -k --connect-timeout 5 https://git.lab/ >/dev/null 2>&1; then
local current_cert_info=$(echo | openssl s_client -servername git.lab -connect git.lab:443 2>/dev/null | openssl x509 -noout -subject -dates 2>/dev/null)
log_info "Current certificate in use: $current_cert_info"
else
log_warning "No HTTPS service responding on git.lab"
fi
return 0
}
function show_usage() {
cat << USAGE
TLS Certificate Deployment Orchestrator
USAGE: $0 [COMMAND] [OPTIONS]
COMMANDS:
full-deploy Complete deployment workflow (default)
deploy-hosts Deploy certificates to SSH hosts only
deploy-containers Deploy certificates to Docker containers only
verify Run verification tests only
generate Generate new certificate
inventory Create network inventory
status Show deployment status
validate Validate current setup
OPTIONS:
-h, --help Show this help message
--skip-hosts Skip host deployment
--skip-containers Skip container deployment
EXAMPLES:
$0 validate # Check current setup
$0 generate # Generate new certificate
$0 inventory # Create network inventory
$0 full-deploy # Deploy to all targets
$0 deploy-hosts # Deploy to hosts only
FILES:
$CERT_FILE Certificate file
$KEY_FILE Private key file
$INVENTORY_FILE Network inventory
$LOG_FILE Deployment log
USAGE
}
function main() {
local command="${1:-validate}"
local skip_hosts=false
local skip_containers=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
show_usage
exit 0
;;
--skip-hosts)
skip_hosts=true
;;
--skip-containers)
skip_containers=true
;;
generate|inventory|deploy-hosts|deploy-containers|verify|status|full-deploy|validate)
command="$1"
;;
*)
if [[ "$1" != "$command" ]]; then
log_error "Unknown option: $1"
show_usage
exit 1
fi
;;
esac
shift
done
case "$command" in
validate)
log_info "Validating certificate setup..."
check_prerequisites
verify_certificate_installation
log_info "Certificate details:"
openssl x509 -in "$CERT_FILE" -text -noout | grep -E "(Subject:|DNS:|IP Address:|Not Before|Not After)"
log_success "Validation completed"
;;
generate)
log_info "Generating new certificate..."
./scripts/generate_certificate.sh
;;
inventory)
log_info "Creating network inventory..."
./scripts/create_inventory.sh
;;
deploy-hosts)
check_prerequisites
log_info "Deploying to hosts..."
./scripts/deploy_to_hosts.sh
;;
deploy-containers)
check_prerequisites
log_info "Deploying to containers..."
./scripts/deploy_to_containers.sh
;;
verify)
check_prerequisites
verify_certificate_installation
;;
status)
log_info "Certificate deployment status:"
if [[ -f "$CERT_FILE" ]]; then
log_info "Certificate: $(openssl x509 -in "$CERT_FILE" -noout -subject -dates | tr '\n' ' ')"
else
log_warning "Certificate file not found"
fi
if [[ -f "$INVENTORY_FILE" ]]; then
local host_count=$(grep -c "- ip:" "$INVENTORY_FILE" 2>/dev/null || echo "0")
local container_count=$(grep -c "- name:" "$INVENTORY_FILE" 2>/dev/null || echo "0")
log_info "Inventory: $host_count hosts, $container_count containers"
else
log_warning "Inventory file not found"
fi
;;
full-deploy)
log_info "Starting full certificate deployment..."
check_prerequisites
local deployment_success=true
# Deploy to hosts
if [[ "$skip_hosts" != true ]]; then
if ! ./scripts/deploy_to_hosts.sh; then
deployment_success=false
fi
fi
# Deploy to containers
if [[ "$skip_containers" != true ]]; then
if ! ./scripts/deploy_to_containers.sh; then
deployment_success=false
fi
fi
if [[ "$deployment_success" == true ]]; then
log_success "Full deployment completed successfully!"
else
log_error "Deployment completed with errors. Check log for details."
exit 1
fi
;;
*)
log_error "Unknown command: $command"
show_usage
exit 1
;;
esac
}
# Check if script is being executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]] || [[ "${(%):-%N}" == "${0:t}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,79 @@
#!/bin/zsh
# Network inventory generator for TLS certificate deployment
set -e
INVENTORY_FILE="inventory/network_inventory.yaml"
echo "Creating network inventory for certificate deployment..."
# Initialize inventory file
cat > "$INVENTORY_FILE" << 'YAML_START'
# TLS Certificate Deployment Inventory
# Generated: $(date -Iseconds)
network_info:
primary_ip: "192.168.50.210"
subnets:
- "192.168.50.0/24"
- "10.0.32.0/24"
- "10.0.43.0/24"
- "10.0.15.0/24"
# SSH-accessible hosts (exclude current host)
hosts:
YAML_START
# Scan for SSH-accessible hosts in the primary subnet
echo "Scanning for SSH-accessible hosts on 192.168.50.x..."
for i in {1..254}; do
ip="192.168.50.$i"
if [[ "$ip" != "192.168.50.210" ]]; then # Skip current host
if timeout 2 nc -z "$ip" 22 2>/dev/null; then
# Try to identify OS type
os_info=$(timeout 5 ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=3 "$ip" 'uname -s 2>/dev/null || echo "unknown"' 2>/dev/null || echo "ssh_failed")
echo " - ip: \"$ip\"" >> "$INVENTORY_FILE"
echo " ssh_accessible: true" >> "$INVENTORY_FILE"
echo " os_type: \"$os_info\"" >> "$INVENTORY_FILE"
echo " cert_paths:" >> "$INVENTORY_FILE"
echo " ca_certificates: \"/usr/local/share/ca-certificates/\"" >> "$INVENTORY_FILE"
echo " ssl_certs: \"/etc/ssl/git.lab/\"" >> "$INVENTORY_FILE"
echo "Found SSH host: $ip ($os_info)"
fi
fi
done
# Add Docker containers section
echo "" >> "$INVENTORY_FILE"
echo "# Docker containers (excluding Coolify-managed)" >> "$INVENTORY_FILE"
echo "containers:" >> "$INVENTORY_FILE"
# Get running containers, excluding Coolify-managed ones
docker ps --format "{{.Names}}\t{{.Image}}\t{{.ID}}" | while IFS=$'\t' read -r name image id; do
# Skip containers that appear to be Coolify-managed
if [[ ! "$name" =~ "vsgoso0skoo8ss08kg0ogcgo" ]] && [[ ! "$image" =~ "coolify" ]]; then
# Check if container has bash/sh
shell="sh"
if docker exec "$id" which bash >/dev/null 2>&1; then
shell="bash"
fi
echo " - name: \"$name\"" >> "$INVENTORY_FILE"
echo " id: \"$id\"" >> "$INVENTORY_FILE"
echo " image: \"$image\"" >> "$INVENTORY_FILE"
echo " shell: \"$shell\"" >> "$INVENTORY_FILE"
echo " cert_paths:" >> "$INVENTORY_FILE"
echo " ca_certificates: \"/usr/local/share/ca-certificates/\"" >> "$INVENTORY_FILE"
echo "Found container: $name ($image)"
fi
done
echo "" >> "$INVENTORY_FILE"
echo "# Certificate configuration" >> "$INVENTORY_FILE"
echo "certificate:" >> "$INVENTORY_FILE"
echo " domain: \"git.lab\"" >> "$INVENTORY_FILE"
echo " cert_file: \"git.lab.crt\"" >> "$INVENTORY_FILE"
echo " key_file: \"git.lab.key\"" >> "$INVENTORY_FILE"
echo " validity_days: 365" >> "$INVENTORY_FILE"
echo "Inventory created: $INVENTORY_FILE"

View File

@@ -0,0 +1,205 @@
#!/bin/zsh
# TLS Certificate deployment to Docker containers
set -e
CERT_FILE="certificates/git.lab.crt"
KEY_FILE="certificates/git.lab.key"
DOMAIN="git.lab"
function log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
function is_coolify_managed() {
local container_name="$1"
local container_id="$2"
# Check if container name contains Coolify patterns
if [[ "$container_name" =~ "vsgoso0skoo8ss08kg0ogcgo" ]] || [[ "$container_name" =~ "coolify" ]]; then
return 0 # true - is Coolify managed
fi
# Check if container has volumes mounted from /data/coolify/
local mounts=$(docker inspect "$container_id" --format '{{range .Mounts}}{{.Source}}:{{.Destination}} {{end}}' 2>/dev/null || echo "")
if [[ "$mounts" =~ "/data/coolify/" ]]; then
return 0 # true - is Coolify managed
fi
return 1 # false - not Coolify managed
}
function deploy_to_container() {
local container_name="$1"
local container_id="$2"
local container_shell="$3"
log "Deploying certificate to container: $container_name ($container_id)"
# Skip Coolify-managed containers
if is_coolify_managed "$container_name" "$container_id"; then
log "SKIPPED: $container_name is Coolify-managed"
return 0
fi
# Test if container is running
if ! docker exec "$container_id" echo "Container test successful" >/dev/null 2>&1; then
log "ERROR: Cannot execute commands in container $container_name"
return 1
fi
# Copy certificate file to container
log "Copying certificate to container $container_name"
if ! docker cp "$CERT_FILE" "$container_id:/tmp/git.lab.crt"; then
log "ERROR: Failed to copy certificate to $container_name"
return 1
fi
# Install certificate inside container
docker exec "$container_id" $container_shell -c '
# Check if we have the necessary tools
if ! command -v update-ca-certificates >/dev/null 2>&1; then
echo "Installing ca-certificates..."
# Try different package managers
if command -v apt-get >/dev/null 2>&1; then
apt-get update -qq && apt-get install -y ca-certificates
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache ca-certificates
elif command -v yum >/dev/null 2>&1; then
yum install -y ca-certificates
else
echo "Cannot install ca-certificates - unsupported package manager"
exit 1
fi
fi
# Create certificates directory and install certificate
mkdir -p /usr/local/share/ca-certificates/
cp /tmp/git.lab.crt /usr/local/share/ca-certificates/git.lab.crt
chmod 644 /usr/local/share/ca-certificates/git.lab.crt
# Update certificate store
update-ca-certificates
# Clean up
rm -f /tmp/git.lab.crt
echo "Certificate installed in container successfully"
' 2>&1
if [[ $? -eq 0 ]]; then
log "SUCCESS: Certificate deployed to container $container_name"
# Try to find and restart application processes (optional)
log "Checking for application processes to restart in $container_name"
docker exec "$container_id" $container_shell -c '
# Look for common application processes that might need restarting
# This is optional and failure here should not fail the deployment
for proc in node python java nginx apache2 httpd; do
if pgrep "$proc" >/dev/null 2>&1; then
echo "Found $proc processes - consider restarting application if needed"
fi
done
' 2>/dev/null || true
return 0
else
log "ERROR: Failed to deploy certificate to container $container_name"
return 1
fi
}
function main() {
if [[ ! -f "$CERT_FILE" ]]; then
log "ERROR: Certificate file not found. Run generate_certificate.sh first."
exit 1
fi
# Read inventory file for containers
if [[ ! -f "inventory/network_inventory.yaml" ]]; then
log "ERROR: Network inventory file not found. Run create_inventory.sh first."
exit 1
fi
log "Starting certificate deployment to Docker containers..."
# Extract container info from inventory
local containers_section=false
local containers=()
local container_ids=()
local container_shells=()
while IFS= read -r line; do
if [[ "$line" =~ "^containers:" ]]; then
containers_section=true
continue
elif [[ "$line" =~ "^[a-zA-Z].*:" ]] && [[ "$containers_section" == true ]]; then
# End of containers section
break
fi
if [[ "$containers_section" == true ]]; then
if [[ "$line" =~ "- name:" ]]; then
local name=$(echo "$line" | sed 's/.*name: "\([^"]*\)".*/\1/')
containers+=("$name")
elif [[ "$line" =~ "id:" ]]; then
local id=$(echo "$line" | sed 's/.*id: "\([^"]*\)".*/\1/')
container_ids+=("$id")
elif [[ "$line" =~ "shell:" ]]; then
local shell=$(echo "$line" | sed 's/.*shell: "\([^"]*\)".*/\1/')
container_shells+=("$shell")
fi
fi
done < inventory/network_inventory.yaml
if [[ ${#containers[@]} -eq 0 ]]; then
log "No containers found in inventory file"
exit 0
fi
log "Found ${#containers[@]} containers to deploy to"
local success_count=0
local failed_containers=()
local skipped_count=0
# Deploy to each container
for i in {1..${#containers[@]}}; do
local name="${containers[$i]}"
local id="${container_ids[$i]}"
local shell="${container_shells[$i]}"
# Verify container is still running
if ! docker ps --format "{{.Names}}" | grep -q "^${name}$"; then
log "WARNING: Container $name is no longer running, skipping"
((skipped_count++))
continue
fi
if deploy_to_container "$name" "$id" "$shell"; then
((success_count++))
else
failed_containers+=("$name")
fi
done
log "Deployment summary:"
log " Successful: $success_count"
log " Failed: ${#failed_containers[@]}"
log " Skipped: $skipped_count"
if [[ ${#failed_containers[@]} -gt 0 ]]; then
log "Failed containers:"
for container in "${failed_containers[@]}"; do
log " - $container"
done
exit 1
fi
log "All containers processed successfully!"
}
# Check if running directly or being sourced
if [[ "${BASH_SOURCE[0]}" == "${0}" ]] || [[ "${(%):-%N}" == "${0:t}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,130 @@
#!/bin/zsh
# TLS Certificate deployment to SSH-accessible hosts
set -e
CERT_FILE="certificates/git.lab.crt"
KEY_FILE="certificates/git.lab.key"
DOMAIN="git.lab"
function log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
function deploy_to_host() {
local host_ip="$1"
local temp_dir="/tmp/git-lab-cert-$$"
log "Deploying certificate to host: $host_ip"
# Test SSH connectivity
if ! timeout 5 ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=3 "$host_ip" 'echo "SSH test successful"' >/dev/null 2>&1; then
log "ERROR: Cannot connect to $host_ip via SSH"
return 1
fi
# Create temporary directory on remote host
ssh -o StrictHostKeyChecking=no "$host_ip" "mkdir -p $temp_dir"
# Copy certificate files
log "Copying certificate files to $host_ip"
scp -o StrictHostKeyChecking=no "$CERT_FILE" "$host_ip:$temp_dir/git.lab.crt"
scp -o StrictHostKeyChecking=no "$KEY_FILE" "$host_ip:$temp_dir/git.lab.key"
# Execute installation commands on remote host
ssh -o StrictHostKeyChecking=no "$host_ip" << REMOTE_EOF
# Install CA certificate
sudo mkdir -p /usr/local/share/ca-certificates/
sudo cp $temp_dir/git.lab.crt /usr/local/share/ca-certificates/git.lab.crt
sudo chmod 644 /usr/local/share/ca-certificates/git.lab.crt
sudo update-ca-certificates
# Install SSL certificate for web services
sudo mkdir -p /etc/ssl/git.lab/
sudo cp $temp_dir/git.lab.crt /etc/ssl/git.lab/git.lab.crt
sudo cp $temp_dir/git.lab.key /etc/ssl/git.lab/git.lab.key
sudo chmod 644 /etc/ssl/git.lab/git.lab.crt
sudo chmod 600 /etc/ssl/git.lab/git.lab.key
sudo chown root:root /etc/ssl/git.lab/git.lab.*
# Try to reload web services (graceful reload, not restart)
if command -v systemctl >/dev/null 2>&1; then
# Check for common web servers and reload if running
for service in nginx apache2 httpd; do
if systemctl is-active --quiet \$service 2>/dev/null; then
echo "Reloading \$service..."
sudo systemctl reload \$service || echo "Failed to reload \$service"
fi
done
fi
# Clean up
rm -rf $temp_dir
echo "Certificate deployment completed on \$(hostname)"
REMOTE_EOF
if [[ $? -eq 0 ]]; then
log "SUCCESS: Certificate deployed to $host_ip"
return 0
else
log "ERROR: Failed to deploy certificate to $host_ip"
return 1
fi
}
function main() {
if [[ ! -f "$CERT_FILE" ]] || [[ ! -f "$KEY_FILE" ]]; then
log "ERROR: Certificate files not found. Run generate_certificate.sh first."
exit 1
fi
# Read inventory file for hosts
if [[ ! -f "inventory/network_inventory.yaml" ]]; then
log "ERROR: Network inventory file not found. Run create_inventory.sh first."
exit 1
fi
log "Starting certificate deployment to hosts..."
# Extract host IPs from inventory (simple grep-based parsing)
local hosts=($(grep -E "^\s*- ip:" inventory/network_inventory.yaml | sed 's/.*ip: "\([^"]*\)".*/\1/'))
if [[ ${#hosts[@]} -eq 0 ]]; then
log "No hosts found in inventory file"
exit 0
fi
log "Found ${#hosts[@]} hosts to deploy to"
local success_count=0
local failed_hosts=()
# Deploy to each host
for host in "${hosts[@]}"; do
if deploy_to_host "$host"; then
((success_count++))
else
failed_hosts+=("$host")
fi
done
log "Deployment summary:"
log " Successful: $success_count/${#hosts[@]}"
log " Failed: ${#failed_hosts[@]}"
if [[ ${#failed_hosts[@]} -gt 0 ]]; then
log "Failed hosts:"
for host in "${failed_hosts[@]}"; do
log " - $host"
done
exit 1
fi
log "All hosts deployed successfully!"
}
# Check if running directly or being sourced
if [[ "${BASH_SOURCE[0]}" == "${0}" ]] || [[ "${(%):-%N}" == "${0:t}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,89 @@
#!/bin/zsh
# TLS Certificate Generator for git.lab
set -e
CERT_DIR="certificates"
DOMAIN="git.lab"
KEY_FILE="$CERT_DIR/$DOMAIN.key"
CERT_FILE="$CERT_DIR/$DOMAIN.crt"
CSR_FILE="$CERT_DIR/$DOMAIN.csr"
VALIDITY_DAYS=365
echo "Generating TLS certificate for $DOMAIN..."
# Create certificate directory if it doesn't exist
mkdir -p "$CERT_DIR"
# Generate private key
echo "Generating private key..."
openssl genrsa -out "$KEY_FILE" 4096
# Create certificate configuration
cat > "$CERT_DIR/cert.conf" << CONF_EOF
[req]
default_bits = 4096
prompt = no
distinguished_name = dn
req_extensions = v3_req
[dn]
CN = $DOMAIN
O = Internal Lab
OU = DevOps
L = Local
ST = Local
C = US
[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = $DOMAIN
DNS.2 = *.$DOMAIN
IP.1 = 192.168.50.210
CONF_EOF
# Generate certificate signing request
echo "Generating certificate signing request..."
openssl req -new -key "$KEY_FILE" -out "$CSR_FILE" -config "$CERT_DIR/cert.conf"
# Generate self-signed certificate
echo "Generating self-signed certificate..."
openssl x509 -req -in "$CSR_FILE" -signkey "$KEY_FILE" -out "$CERT_FILE" \
-days $VALIDITY_DAYS -extensions v3_req -extfile "$CERT_DIR/cert.conf"
# Set proper permissions
chmod 600 "$KEY_FILE"
chmod 644 "$CERT_FILE"
# Display certificate information
echo "Certificate generated successfully!"
echo "Certificate file: $CERT_FILE"
echo "Private key file: $KEY_FILE"
echo ""
echo "Certificate details:"
openssl x509 -in "$CERT_FILE" -text -noout | grep -E "(Subject:|DNS:|IP Address:|Not Before|Not After)"
# Copy to Coolify directory if it exists
if [[ -d "/data/coolify/proxy/certificates" ]]; then
echo ""
echo "Copying certificate to Coolify directory..."
sudo cp "$CERT_FILE" "/data/coolify/proxy/certificates/"
sudo cp "$KEY_FILE" "/data/coolify/proxy/certificates/"
sudo chown root:root "/data/coolify/proxy/certificates/$DOMAIN.crt"
sudo chown root:root "/data/coolify/proxy/certificates/$DOMAIN.key"
sudo chmod 644 "/data/coolify/proxy/certificates/$DOMAIN.crt"
sudo chmod 600 "/data/coolify/proxy/certificates/$DOMAIN.key"
echo "Certificate installed in Coolify proxy directory."
else
echo "Coolify directory not found - certificate only stored locally."
fi
# Clean up CSR file
rm -f "$CSR_FILE"
rm -f "$CERT_DIR/cert.conf"
echo "Certificate generation complete!"

View File

@@ -0,0 +1,117 @@
#!/bin/zsh
cd ~/tls-cert-manager
echo "=== Certificate Validation Report ==="
echo
# 1. Check certificate files exist and have correct permissions
echo "1. Certificate Files:"
if [[ -f "certificates/git.lab.crt" ]]; then
echo " ✓ Certificate file exists"
ls -la certificates/git.lab.crt | awk '{print " Permissions:", $1, "Owner:", $3":"$4}'
else
echo " ✗ Certificate file missing"
fi
if [[ -f "certificates/git.lab.key" ]]; then
echo " ✓ Private key exists"
ls -la certificates/git.lab.key | awk '{print " Permissions:", $1, "Owner:", $3":"$4}'
else
echo " ✗ Private key missing"
fi
echo
# 2. Validate certificate content
echo "2. Certificate Validation:"
if openssl x509 -in certificates/git.lab.crt -noout -text >/dev/null 2>&1; then
echo " ✓ Certificate format is valid"
# Check expiration
if openssl x509 -in certificates/git.lab.crt -checkend 86400 >/dev/null 2>&1; then
echo " ✓ Certificate is not expired (valid for >24h)"
else
echo " ⚠ Certificate expires within 24 hours"
fi
# Show certificate details
echo " Certificate Details:"
openssl x509 -in certificates/git.lab.crt -text -noout | grep -E "(Subject:|DNS:|IP Address:|Not Before|Not After)" | sed 's/^/ /'
else
echo " ✗ Certificate format is invalid"
fi
echo
# 3. Check script permissions and existence
echo "3. Script Files:"
for script in create_inventory.sh generate_certificate.sh deploy_to_hosts.sh deploy_to_containers.sh cert_deployment_orchestrator.sh; do
if [[ -f "scripts/$script" ]]; then
if [[ -x "scripts/$script" ]]; then
echo "$script (executable)"
else
echo "$script (not executable)"
fi
else
echo "$script (missing)"
fi
done
echo
# 4. Check network connectivity to git.lab
echo "4. Network Connectivity:"
if nslookup git.lab >/dev/null 2>&1; then
local_ip=$(nslookup git.lab | grep -A1 "Name:" | grep "Address:" | awk '{print $2}')
echo " ✓ git.lab resolves to: $local_ip"
if curl -k --connect-timeout 5 https://git.lab/ >/dev/null 2>&1; then
echo " ✓ HTTPS service is responding"
# Check current certificate
current_cert=$(echo | openssl s_client -servername git.lab -connect git.lab:443 2>/dev/null | openssl x509 -noout -subject -dates 2>/dev/null)
if [[ -n "$current_cert" ]]; then
echo " Current certificate in use:"
echo "$current_cert" | sed 's/^/ /'
fi
else
echo " ⚠ HTTPS service not responding or not accessible"
fi
else
echo " ✗ git.lab does not resolve"
fi
echo
# 5. Check for Coolify integration
echo "5. Coolify Integration:"
if [[ -d "/data/coolify/proxy/certificates" ]]; then
echo " ✓ Coolify certificates directory exists"
echo " Current certificates in Coolify:"
sudo ls -la /data/coolify/proxy/certificates/ | grep -E "\.(crt|key)$" | sed 's/^/ /'
else
echo " ⚠ Coolify certificates directory not found"
fi
echo
# 6. Check Docker containers (if Docker is available)
echo "6. Docker Environment:"
if command -v docker >/dev/null 2>&1; then
echo " ✓ Docker is available"
container_count=$(docker ps --format "{{.Names}}" 2>/dev/null | wc -l)
echo " Running containers: $container_count"
if [[ $container_count -gt 0 ]]; then
echo " Active containers:"
docker ps --format "table {{.Names}}\t{{.Image}}" | head -5 | tail -n +2 | sed 's/^/ /'
if [[ $container_count -gt 4 ]]; then
echo " ... and $((container_count - 4)) more"
fi
fi
else
echo " ⚠ Docker not available"
fi
echo
echo "=== Validation Complete ==="

37
terraform/AGENTS.md Normal file
View File

@@ -0,0 +1,37 @@
# Repository Guidelines
## Project Structure & Module Organization
- Core manifests: `main.tf` wires providers, `workspace.tf` defines the agent, and `services.tf`/`apps.tf` supply optional containers and apps.citeturn0search11
- `scripts.tf` maps Terraform to `scripts/*.sh`; add entries to `locals.workspace_agent_scripts` and keep those scripts idempotent because agents rerun them.
- `terraform.tfvars` carries defaults; store environment overrides in separate `*.auto.tfvars`.
- Workspace image comes from `../.devcontainer/Dockerfile`; after edits rebuild, retag, and bump `var.devcontainer_image` so Terraform targets a ready registry image.citeturn0search5
## Build, Test, and Development Commands
- `terraform init` — rerun after changing `required_providers`.
- `terraform fmt -recursive` — enforce two-space indentation before committing.
- `terraform validate` — catch schema or variable issues early.
- `terraform plan -var-file=terraform.tfvars -out plan.tfplan` — review planned changes; share the summary in reviews.
- `docker build -t <registry>/<image>:<tag> -f ../.devcontainer/Dockerfile ..` then `docker push` — refresh the devcontainer base before updating `devcontainer_image`.citeturn0search5
## Coding Style & Naming Conventions
- Follow Terraform style: snake_case for locals/variables, singular resource names (`coder_agent.main`), and kebab-case for Docker/Coder `name`/`slug` fields.
- Group locals by concern and comment non-obvious transformations (e.g., startup script assembly).
- Keep `required_providers` aligned with Coder template defaults so `coder` and `docker` stay pinned.citeturn0search11
- Bash scripts must start with `#!/usr/bin/env bash` and `set -euo pipefail`; prefer functions for reusable logic.
## Testing Guidelines
- Run `terraform fmt -check` and `terraform validate` locally and in CI.
- Exercise feature toggles with targeted plans, e.g. `terraform plan -var enable_services=false`.
- When services are enabled, run `bash scripts/port-forward.sh` inside the workspace to confirm pgAdmin and Qdrant forwards.
- Skip committing `plan.tfplan`; attach `terraform show plan.tfplan` output in PRs.
## Commit & Pull Request Guidelines
- History is absent here; default to Conventional Commits (`feat:`, `fix:`, `chore:`) used across Coder templates.
- Scope commits narrowly and reference modules in the subject, e.g. `fix: tighten docker socket handling in workspace.tf`.
- PRs should note the user impact, include relevant `terraform plan` excerpts, call out service toggle defaults, and link tracking issues.
## Security & Configuration Tips
- Never commit secrets: provide `gitea_pat`, `github_pat`, or service passwords via overrides or template environment variables.
- When mount paths or ports move, update matching Terraform locals and scripts so port forwarding and health checks stay aligned.
- Rebuild the devcontainer image separately; this module assumes `var.devcontainer_image` already bundles required toolchains.
- Rely on registry scanning and caching; Coder guidance expects hardened, prebuilt images before workspaces launch.citeturn0search2

View File

@@ -4,7 +4,7 @@ This Terraform module provisions a Coder workspace that mirrors the devcontainer
## What You Get
- One Docker workspace container built from `var.devcontainer_image` (defaults to the universal Dev Container image).
- One Docker workspace container built from `var.devcontainer_image` (defaults to `git.lab/vasceannie/golden-image:latest`).
- Optional PostgreSQL, Redis, and Qdrant services running on the same Docker network, plus pgAdmin and Jupyter toggles.
- Startup scripts that install core tooling and (optionally) AI helpers for Claude, Cursor, and Windsurf.
- A trimmed Coder application list (VS Code, Terminal, pgAdmin, Qdrant, Jupyter, and a few common dev ports).
@@ -13,7 +13,7 @@ This Terraform module provisions a Coder workspace that mirrors the devcontainer
| Name | Description | Default |
| --- | --- | --- |
| `devcontainer_image` | Workspace container image | `mcr.microsoft.com/devcontainers/universal:2-linux` |
| `devcontainer_image` | Workspace container image | `git.lab/vasceannie/golden-image:latest` |
| `workspace_memory_limit` | Memory limit in MB (0 = image default) | `8192` |
| `enable_docker_in_docker` | Mount `/var/run/docker.sock` | `true` |
| `postgres_password` / `redis_password` | Service credentials | `devpassword` |

View File

@@ -1,19 +1,3 @@
resource "coder_app" "code_server" {
agent_id = coder_agent.main.id
slug = "code-server"
display_name = "VS Code"
url = "http://localhost:13337?folder=/workspaces"
icon = "/icon/code.svg"
subdomain = false
share = "owner"
healthcheck {
url = "http://localhost:13337/healthz"
interval = 10
threshold = 5
}
}
resource "coder_app" "pgadmin" {
count = local.services_enabled && data.coder_parameter.enable_pgadmin.value ? 1 : 0
agent_id = coder_agent.main.id
@@ -36,7 +20,7 @@ resource "coder_app" "qdrant" {
agent_id = coder_agent.main.id
slug = "qdrant"
display_name = "Qdrant"
url = "http://qdrant-${local.workspace_id}:6333/dashboard"
url = "http://qdrant-${local.workspace_id}:6333"
icon = "/icon/database.svg"
subdomain = false
share = "owner"
@@ -58,12 +42,6 @@ resource "coder_app" "marimo" {
subdomain = false
share = "owner"
group = "Development Services"
healthcheck {
url = "http://localhost:8888"
interval = 20
threshold = 10
}
}
locals {
@@ -87,7 +65,7 @@ locals {
}
resource "coder_app" "dev_ports" {
for_each = local.dev_ports
for_each = local.dev_endpoints_enabled ? local.dev_ports : {}
agent_id = coder_agent.main.id
slug = each.key
@@ -97,12 +75,6 @@ resource "coder_app" "dev_ports" {
subdomain = true
share = "owner"
group = "Development Services"
healthcheck {
url = each.value.url
interval = 10
threshold = 10
}
}
resource "coder_app" "claude_cli" {

View File

@@ -82,6 +82,16 @@ data "coder_parameter" "enable_marimo" {
order = 5
}
data "coder_parameter" "enable_dev_endpoints" {
name = "enable_dev_endpoints"
display_name = "Expose Dev HTTP Ports"
description = "Create Development Services app shortcuts for localhost:3000, 5173, and 8000."
type = "bool"
default = "false"
mutable = true
order = 6
}
data "coder_parameter" "enable_jetbrains" {
name = "enable_jetbrains"
display_name = "JetBrains Gateway"
@@ -89,7 +99,7 @@ data "coder_parameter" "enable_jetbrains" {
type = "bool"
default = "true"
mutable = true
order = 6
order = 7
}
data "coder_parameter" "ai_prompt" {
@@ -99,7 +109,7 @@ data "coder_parameter" "ai_prompt" {
type = "string"
default = ""
mutable = true
order = 7
order = 8
form_type = "textarea"
}
@@ -115,14 +125,16 @@ locals {
git_author_name = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
git_author_email = data.coder_workspace_owner.me.email
project_repo_url = trimspace(data.coder_parameter.project_repository.value)
gitea_pat = trimspace(var.gitea_pat)
github_pat = trimspace(var.github_pat)
project_repo_url = trimspace(data.coder_parameter.project_repository.value)
gitea_pat = trimspace(var.gitea_pat)
github_pat = trimspace(var.github_pat)
services_enabled = data.coder_parameter.enable_services.value
pgadmin_enabled = data.coder_parameter.enable_pgadmin.value
marimo_enabled = data.coder_parameter.enable_marimo.value
port_forwarding = local.services_enabled || local.marimo_enabled
services_enabled = data.coder_parameter.enable_services.value
pgadmin_enabled = data.coder_parameter.enable_pgadmin.value
marimo_enabled = data.coder_parameter.enable_marimo.value
dev_endpoints_enabled = data.coder_parameter.enable_dev_endpoints.value
ai_enabled = data.coder_parameter.enable_ai_tools.value
port_forwarding = local.services_enabled || local.marimo_enabled
postgres_url = "postgresql://postgres:${var.postgres_password}@postgres-${local.workspace_id}:5432/postgres"
redis_url = "redis://:${var.redis_password}@redis-${local.workspace_id}:6379"
@@ -132,20 +144,31 @@ locals {
"set -eu",
"export CODER_WORKSPACE_ID=${local.workspace_id}",
"# Fix RVM environment variables to suppress warnings",
"if printf '%s' \"$PATH\" | tr ':' '\n' | grep -q '/usr/share/rvm'; then",
" OLD_IFS=$IFS",
" IFS=':'",
" NEW_PATH=\"\"",
" for segment in $$PATH; do",
" case \"$segment\" in",
" *'/usr/share/rvm'* ) continue ;;",
" * ) if [ -z \"$$NEW_PATH\" ]; then NEW_PATH=\"$$segment\"; else NEW_PATH=\"$$NEW_PATH:$$segment\"; fi ;;",
" esac",
" done",
" IFS=$OLD_IFS",
" if [ -n \"$$NEW_PATH\" ]; then export PATH=\"$$NEW_PATH\"; fi",
"fi",
"export GEM_HOME=\"$HOME/.gem\"",
"export GEM_PATH=\"$HOME/.gem\"",
"export rvm_silence_path_mismatch_check_flag=1",
"export RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG=1",
"export rvmsudo_secure_path=1",
"export RVMSUDO_SECURE_PATH=1",
"# Ensure required directories exist",
"mkdir -p /home/coder/code-tools/terraform/scripts",
"mkdir -p /home/coder/code-tools/terraform/scripts/agentapi",
"# Ensure Python 3.12 is available",
"if ! command -v python3 >/dev/null 2>&1 || ! python3 -c 'import sys; exit(0) if sys.version_info >= (3, 12) else exit(1)' ; then",
" echo 'Installing Python 3.12...'",
" apt-get update -qq",
" apt-get install -y software-properties-common",
" add-apt-repository ppa:deadsnakes/ppa -y",
" apt-get update -qq",
" apt-get install -y python3.12 python3.12-venv python3.12-dev python3-pip",
" update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1",
"# Verify Python 3.12+ baked into the image (checking as coder user)",
"if ! sudo --preserve-env=CODER_WORKSPACE_ID,CODER_WORKSPACE_REPO,GITEA_PAT,GITHUB_PAT -u coder env -i HOME=/home/coder PATH=\"$PATH\" GEM_HOME=/home/coder/.gem GEM_PATH=/home/coder/.gem CODER_WORKSPACE_ID=\"$${CODER_WORKSPACE_ID-}\" CODER_WORKSPACE_REPO=\"$${CODER_WORKSPACE_REPO-}\" GITEA_PAT=\"$${GITEA_PAT-}\" GITHUB_PAT=\"$${GITHUB_PAT-}\" rvm_silence_path_mismatch_check_flag=1 RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG=1 rvmsudo_secure_path=1 RVMSUDO_SECURE_PATH=1 bash -lc 'command -v python3 >/dev/null 2>&1 && python3 -c \"import sys; exit(0) if sys.version_info >= (3, 12) else exit(1)\"'; then",
" echo 'Python 3.12+ not detected for coder user; update the base image.'",
"fi",
"mkdir -p /workspaces",
"chown -R coder:coder /workspaces || echo 'Cannot change workspace ownership'",
@@ -156,12 +179,12 @@ locals {
" cp /home/coder/lab-certs/lab.crt /usr/local/share/ca-certificates/lab.crt 2>/dev/null && echo 'SSL cert copied successfully' || echo 'Cannot copy SSL cert'",
" update-ca-certificates 2>/dev/null && echo 'CA certificates updated successfully' || echo 'Cannot update ca-certificates'",
" # Configure git globally for coder user",
" sudo -u coder git config --global http.\"https://git.lab\".sslCAInfo /home/coder/lab-certs/lab.crt || echo 'Cannot configure git ssl'",
" sudo -u coder git config --global http.\"https://git.lab\".sslVerify true || echo 'Cannot configure git ssl verify'",
" sudo --preserve-env=CODER_WORKSPACE_ID,CODER_WORKSPACE_REPO,GITEA_PAT,GITHUB_PAT -u coder env -i HOME=/home/coder PATH=\"$PATH\" GEM_HOME=/home/coder/.gem GEM_PATH=/home/coder/.gem CODER_WORKSPACE_ID=\"$${CODER_WORKSPACE_ID-}\" CODER_WORKSPACE_REPO=\"$${CODER_WORKSPACE_REPO-}\" GITEA_PAT=\"$${GITEA_PAT-}\" GITHUB_PAT=\"$${GITHUB_PAT-}\" rvm_silence_path_mismatch_check_flag=1 RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG=1 rvmsudo_secure_path=1 RVMSUDO_SECURE_PATH=1 git config --global http.\"https://git.lab\".sslCAInfo /home/coder/lab-certs/lab.crt || echo 'Cannot configure git ssl'",
" sudo --preserve-env=CODER_WORKSPACE_ID,CODER_WORKSPACE_REPO,GITEA_PAT,GITHUB_PAT -u coder env -i HOME=/home/coder PATH=\"$PATH\" GEM_HOME=/home/coder/.gem GEM_PATH=/home/coder/.gem CODER_WORKSPACE_ID=\"$${CODER_WORKSPACE_ID-}\" CODER_WORKSPACE_REPO=\"$${CODER_WORKSPACE_REPO-}\" GITEA_PAT=\"$${GITEA_PAT-}\" GITHUB_PAT=\"$${GITHUB_PAT-}\" rvm_silence_path_mismatch_check_flag=1 RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG=1 rvmsudo_secure_path=1 RVMSUDO_SECURE_PATH=1 git config --global http.\"https://git.lab\".sslVerify true || echo 'Cannot configure git ssl verify'",
"else",
" echo 'SSL cert not available at /home/coder/lab-certs/lab.crt'",
" # Configure git to skip SSL verification for git.lab if no cert available",
" sudo -u coder git config --global http.\"https://git.lab\".sslVerify false || echo 'Cannot configure git ssl skip'",
" sudo --preserve-env=CODER_WORKSPACE_ID,CODER_WORKSPACE_REPO,GITEA_PAT,GITHUB_PAT -u coder env -i HOME=/home/coder PATH=\"$PATH\" GEM_HOME=/home/coder/.gem GEM_PATH=/home/coder/.gem CODER_WORKSPACE_ID=\"$${CODER_WORKSPACE_ID-}\" CODER_WORKSPACE_REPO=\"$${CODER_WORKSPACE_REPO-}\" GITEA_PAT=\"$${GITEA_PAT-}\" GITHUB_PAT=\"$${GITHUB_PAT-}\" rvm_silence_path_mismatch_check_flag=1 RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG=1 rvmsudo_secure_path=1 RVMSUDO_SECURE_PATH=1 git config --global http.\"https://git.lab\".sslVerify false || echo 'Cannot configure git ssl skip'",
"fi",
"# End SSL setup",
"export ENABLE_PGADMIN=${tostring(local.pgadmin_enabled)}",

View File

@@ -3,14 +3,14 @@ locals {
workspace = {
display = "Setup Development Workspace"
icon = "/icon/container.svg"
script = "/usr/local/bin/workspace-setup.sh"
path = "${path.module}/scripts/workspace-setup.sh"
order = 1
blocks_login = true
}
dev_tools = {
display = "Install Development Tools"
icon = "/icon/code.svg"
script = "/usr/local/bin/dev-tools.sh"
path = "${path.module}/scripts/dev-tools.sh"
order = 2
blocks_login = true
}
@@ -35,14 +35,14 @@ locals {
enabled = data.coder_parameter.enable_ai_tools.value && var.install_claude_code
display = "Install Claude CLI"
icon = "/icon/claude.svg"
script = "/usr/local/bin/claude-install.sh"
path = "${path.module}/scripts/claude-install.sh"
blocks_login = false
}
codex = {
enabled = data.coder_parameter.enable_ai_tools.value && var.install_codex_support
display = "Install Codex CLI"
icon = "/icon/openai.svg"
script = "/usr/local/bin/codex-setup.sh"
path = "${path.module}/scripts/codex-setup.sh"
blocks_login = false
}
gemini = {
@@ -56,14 +56,14 @@ locals {
enabled = data.coder_parameter.enable_ai_tools.value && var.install_cursor_support
display = "Configure Cursor"
icon = "/icon/cursor.svg"
script = "/usr/local/bin/cursor-setup.sh"
path = "${path.module}/scripts/cursor-setup.sh"
blocks_login = false
}
windsurf = {
enabled = data.coder_parameter.enable_ai_tools.value && var.install_windsurf_support
display = "Configure Windsurf"
icon = "/icon/windsurf.svg"
script = "/usr/local/bin/windsurf-setup.sh"
path = "${path.module}/scripts/windsurf-setup.sh"
blocks_login = false
}
}
@@ -78,7 +78,35 @@ resource "coder_script" "core" {
run_on_start = true
start_blocks_login = each.value.blocks_login
script = lookup(each.value, "script", null) != null ? "bash ${each.value.script}" : "echo '${base64encode(file(each.value.path))}' | base64 -d | tr -d '\\r' | bash"
script = <<-EOT
set -euo pipefail
script_file=$(mktemp)
echo '${base64encode(file(each.value.path))}' | base64 -d | tr -d '\r' > "$script_file"
chmod +x "$script_file"
export PATH="/home/coder/.venv/bin:/home/coder/.local/bin:/home/coder/bin:/home/coder/.cargo/bin:/usr/local/go/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:$PATH"
current_nvm=$(printenv NVM_DIR 2>/dev/null || true)
if [ -z "$current_nvm" ]; then
NVM_DIR=/usr/local/share/nvm
else
NVM_DIR="$current_nvm"
fi
export NVM_DIR
if [ -d "$NVM_DIR" ]; then
if [ -s "$NVM_DIR/nvm.sh" ]; then
# shellcheck disable=SC1090,SC1091
. "$NVM_DIR/nvm.sh" >/dev/null 2>&1 || true
fi
shopt -s nullglob
for node_bin in "$NVM_DIR"/versions/node/*/bin; do
PATH="$node_bin:$PATH"
done
shopt -u nullglob
fi
"$script_file"
EOT
}
resource "coder_script" "ai" {
@@ -90,5 +118,33 @@ resource "coder_script" "ai" {
run_on_start = true
start_blocks_login = each.value.blocks_login
script = lookup(each.value, "script", null) != null ? "bash ${each.value.script}" : "echo '${base64encode(file(each.value.path))}' | base64 -d | tr -d '\\r' | bash"
script = <<-EOT
set -euo pipefail
script_file=$(mktemp)
echo '${base64encode(file(each.value.path))}' | base64 -d | tr -d '\r' > "$script_file"
chmod +x "$script_file"
export PATH="/home/coder/.venv/bin:/home/coder/.local/bin:/home/coder/bin:/home/coder/.cargo/bin:/usr/local/go/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:$PATH"
current_nvm=$(printenv NVM_DIR 2>/dev/null || true)
if [ -z "$current_nvm" ]; then
NVM_DIR=/usr/local/share/nvm
else
NVM_DIR="$current_nvm"
fi
export NVM_DIR
if [ -d "$NVM_DIR" ]; then
if [ -s "$NVM_DIR/nvm.sh" ]; then
# shellcheck disable=SC1090,SC1091
. "$NVM_DIR/nvm.sh" >/dev/null 2>&1 || true
fi
shopt -s nullglob
for node_bin in "$NVM_DIR"/versions/node/*/bin; do
PATH="$node_bin:$PATH"
done
shopt -u nullglob
fi
"$script_file"
EOT
}

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[claude-setup] %s\n' "$1"
}
if command -v claude >/dev/null 2>&1; then
version=$(claude --version 2>/dev/null | head -n 1)
if [ -n "$version" ]; then
log "Claude CLI already available: $version"
else
log "Claude CLI already available"
fi
else
log "Claude CLI not bundled in this image. Skipping installation."
fi

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[codex-setup] %s\n' "$1"
}
if command -v codex >/dev/null 2>&1; then
version=$(codex --version 2>/dev/null | head -n 1)
if [ -n "$version" ]; then
log "Codex CLI available: $version"
else
log "Codex CLI available"
fi
else
log "Codex CLI not bundled; skipping installation."
fi

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[cursor-setup] %s\n' "$1"
}
SETTINGS_DIR="${HOME:-/home/coder}/.config/cursor"
mkdir -p "$SETTINGS_DIR"
log "Cursor configuration directory ensured at $SETTINGS_DIR"

121
terraform/scripts/dev-tools.sh Executable file
View File

@@ -0,0 +1,121 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[dev-tools] %s\n' "$1"
}
PATH="/home/coder/.venv/bin:/home/coder/.local/bin:/home/coder/bin:/home/coder/.cargo/bin:/usr/local/go/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:${PATH-}"
export PATH
GO_VERSION="${GO_VERSION:-1.25.1}"
ensure_go() {
if command -v go >/dev/null 2>&1; then
return 0
fi
local go_root="${HOME}/.local/share/go"
local go_bin="${go_root}/bin/go"
if [ -x "${go_bin}" ]; then
PATH="${go_root}/bin:${PATH}"
export PATH
mkdir -p "${HOME}/bin"
ln -sf "${go_root}/bin/go" "${HOME}/bin/go"
ln -sf "${go_root}/bin/gofmt" "${HOME}/bin/gofmt"
return 0
fi
if ! command -v curl >/dev/null 2>&1; then
log "curl not available; cannot install Go automatically"
return 1
fi
local archive="go${GO_VERSION}.linux-amd64.tar.gz"
local url="https://go.dev/dl/${archive}"
local tmp_dir
tmp_dir=$(mktemp -d)
log "Installing Go ${GO_VERSION} locally (missing from base image)"
if curl -fsSL "${url}" -o "${tmp_dir}/${archive}" && tar -C "${tmp_dir}" -xzf "${tmp_dir}/${archive}"; then
local base_dir
base_dir="$(dirname "${go_root}")"
mkdir -p "${base_dir}"
rm -rf "${go_root}"
mv "${tmp_dir}/go" "${go_root}"
PATH="${go_root}/bin:${PATH}"
export PATH
mkdir -p "${HOME}/bin"
ln -sf "${go_root}/bin/go" "${HOME}/bin/go"
ln -sf "${go_root}/bin/gofmt" "${HOME}/bin/gofmt"
rm -rf "${tmp_dir}"
return 0
fi
log "Failed to install Go ${GO_VERSION}; please rebuild the base image"
rm -rf "${tmp_dir}"
return 1
}
ensure_go || true
NVM_DIR="${NVM_DIR:-/usr/local/share/nvm}"
if [ -d "$NVM_DIR" ]; then
if [ -s "$NVM_DIR/nvm.sh" ]; then
# shellcheck disable=SC1090,SC1091
. "$NVM_DIR/nvm.sh" >/dev/null 2>&1 || true
fi
shopt -s nullglob
node_bins=("$NVM_DIR"/versions/node/*/bin)
shopt -u nullglob
if [ ${#node_bins[@]} -gt 0 ]; then
for node_bin in "${node_bins[@]}"; do
PATH="$node_bin:$PATH"
done
fi
fi
export PATH
tools=(node npm pnpm yarn python3 uv go rustc cargo)
failure=0
for tool in "${tools[@]}"; do
if command -v "$tool" >/dev/null 2>&1; then
case "$tool" in
go)
version_output=$(go version 2>/dev/null | head -n 1)
;;
*)
version_output=$("$tool" --version 2>/dev/null | head -n 1)
;;
esac
if [ -n "$version_output" ]; then
log "$tool available: $version_output"
else
log "$tool available"
fi
if [ "$tool" = "python3" ]; then
py_minor=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
if python3 -c 'import sys; exit(0) if sys.version_info >= (3, 12) else exit(1)'; then
log "python3 meets minimum version requirement (>= 3.12)"
else
log "python3 version ${py_minor} is below required 3.12; update the base image"
failure=1
fi
fi
else
log "$tool not found in PATH"
failure=1
fi
done
if [ "$failure" -eq 1 ]; then
log "Developer tooling validation failed"
exit 1
fi
log "Developer tooling check completed."

106
terraform/scripts/marimo-setup.sh Executable file
View File

@@ -0,0 +1,106 @@
#!/usr/bin/env bash
set -euo pipefail
VENV_DIR="${HOME}/.venv/marimo"
MARIMO_VERSION="0.16.0"
NOTEBOOK_ROOT="${HOME}/workspaces/notebooks"
WELCOME_NOTEBOOK="${NOTEBOOK_ROOT}/welcome.py"
PIP_SENTINEL="${VENV_DIR}/.pip-upgraded"
log() {
printf '[marimo] %s\n' "$1"
}
ensure_python() {
if command -v python3.12 >/dev/null 2>&1; then
PYTHON_BIN=$(command -v python3.12)
elif command -v python3 >/dev/null 2>&1; then
PYTHON_BIN=$(command -v python3)
else
log "Python 3.x is not available; skipping Marimo setup"
exit 0
fi
python_version=$($PYTHON_BIN -c 'import sys; print("{}.{}".format(sys.version_info.major, sys.version_info.minor))')
IFS='.' read -r py_major py_minor <<<"$python_version"
if (( py_major < 3 || (py_major == 3 && py_minor < 9) )); then
log "Python version $python_version is unsupported; need >= 3.9"
exit 1
fi
}
create_venv() {
if [[ ! -f "${VENV_DIR}/bin/activate" ]]; then
log "Creating Python virtual environment at ${VENV_DIR}"
mkdir -p "$(dirname "${VENV_DIR}")"
"${PYTHON_BIN}" -m venv "${VENV_DIR}"
else
log "Virtual environment already present at ${VENV_DIR}"
fi
}
upgrade_tools() {
if [[ -f "${PIP_SENTINEL}" ]]; then
log "pip upgrade already performed"
else
log "Upgrading pip and wheel"
"${VENV_DIR}/bin/pip" install --upgrade pip wheel >/dev/null
touch "${PIP_SENTINEL}"
fi
}
install_marimo() {
if "${VENV_DIR}/bin/python" -m marimo --version >/dev/null 2>&1; then
current_version=$("${VENV_DIR}/bin/python" -m marimo --version | awk '{print $NF}')
if [[ "${current_version}" == "${MARIMO_VERSION}" ]]; then
log "Marimo ${current_version} already installed"
return
fi
log "Updating Marimo from ${current_version} to ${MARIMO_VERSION}"
else
log "Installing Marimo ${MARIMO_VERSION}"
fi
"${VENV_DIR}/bin/pip" install --upgrade "marimo==${MARIMO_VERSION}" >/dev/null
}
ensure_notebook() {
mkdir -p "${NOTEBOOK_ROOT}"
if [[ ! -f "${WELCOME_NOTEBOOK}" ]]; then
log "Creating welcome notebook at ${WELCOME_NOTEBOOK}"
cat <<PYEOF > "${WELCOME_NOTEBOOK}"
import marimo
__generated_with = "${MARIMO_VERSION}"
app = marimo.App()
@app.cell
def __():
import marimo as mo
return mo,
@app.cell
def __(mo):
mo.md("# Welcome to Marimo!")
return
@app.cell
def __(mo):
mo.md("This is your interactive notebook environment.")
return
if __name__ == "__main__":
app.run()
PYEOF
else
log "Welcome notebook already exists"
fi
}
ensure_python
create_venv
upgrade_tools
install_marimo
ensure_notebook
log "Marimo environment ready"

View File

@@ -15,7 +15,11 @@ SERVICES_ENABLED="${ENABLE_SERVICES:-false}"
PGADMIN_ENABLED="${ENABLE_PGADMIN:-false}"
MARIMO_ENABLED="${ENABLE_MARIMO:-false}"
if ! command -v socat >/dev/null 2>&1; then
ensure_socat() {
if command -v socat >/dev/null 2>&1; then
return 0
fi
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y socat >/dev/null
@@ -23,25 +27,92 @@ if ! command -v socat >/dev/null 2>&1; then
sudo apk add --no-cache socat >/dev/null
else
echo "socat is required for port forwarding but could not be installed automatically" >&2
exit 0
return 1
fi
}
resolve_target() {
local container_name="$1"
local wait_seconds="${2:-60}"
local sleep_interval=2
local waited=0
local ip=""
while (( waited < wait_seconds )); do
if command -v docker >/dev/null 2>&1; then
if docker inspect -f "{{.State.Running}}" "${container_name}" >/dev/null 2>&1; then
ip="$(docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" "${container_name}" 2>/dev/null | tr -d " ")"
if [[ -n "${ip}" ]]; then
echo "${ip}"
return 0
fi
fi
fi
if getent hosts "${container_name}" >/dev/null 2>&1; then
echo "${container_name}"
return 0
fi
sleep "${sleep_interval}"
waited=$((waited + sleep_interval))
done
return 1
}
start_forward() {
local service_name="$1"
local listen_port="$2"
local target_port="$3"
local log_path="$4"
local label="$5"
local retries="${6:-1}"
local retry_delay="${7:-5}"
local attempt=1
local target
local wait_seconds=90
if [[ "$label" == "pgAdmin" ]]; then
wait_seconds=150
fi
while (( attempt <= retries )); do
if target="$(resolve_target "${service_name}" "$wait_seconds")"; then
echo "Forwarding ${label} to localhost:${listen_port} (target: ${target}:${target_port})"
nohup socat TCP-LISTEN:${listen_port},reuseaddr,fork TCP:${target}:${target_port} >"${log_path}" 2>&1 &
return 0
fi
if (( attempt < retries )); then
echo "Retrying ${label} forward in ${retry_delay}s..." >&2
sleep "${retry_delay}"
fi
attempt=$((attempt + 1))
done
echo "Timed out waiting for ${label} container (${service_name}); skipping forward" >&2
return 1
}
if ! ensure_socat; then
exit 0
fi
# stop previous forwards if they exist
pkill -f "socat.*pgadmin" >/dev/null 2>&1 || true
pkill -f "socat.*qdrant" >/dev/null 2>&1 || true
pkill -f "socat.*5050" >/dev/null 2>&1 || true
pkill -f "socat.*6333" >/dev/null 2>&1 || true
pkill -f "socat.*marimo" >/dev/null 2>&1 || true
if [[ "${SERVICES_ENABLED}" == "true" ]]; then
if [[ "${PGADMIN_ENABLED}" == "true" ]]; then
echo "Forwarding pgAdmin to localhost:5050"
nohup socat TCP-LISTEN:5050,reuseaddr,fork TCP:pgadmin-${WORKSPACE_ID}:80 >/tmp/socat-pgadmin.log 2>&1 &
start_forward "pgadmin-${WORKSPACE_ID}" 5050 5050 /tmp/socat-pgadmin.log "pgAdmin" 2 10 || true
else
echo "pgAdmin disabled; skipping port forward"
fi
echo "Forwarding Qdrant to localhost:6333"
nohup socat TCP-LISTEN:6333,reuseaddr,fork TCP:qdrant-${WORKSPACE_ID}:6333 >/tmp/socat-qdrant.log 2>&1 &
start_forward "qdrant-${WORKSPACE_ID}" 6333 6333 /tmp/socat-qdrant.log "Qdrant" || true
else
echo "Database services disabled; skipping pgAdmin/Qdrant forwards"
fi

90
terraform/scripts/terminal-tools.sh Normal file → Executable file
View File

@@ -1,58 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
echo "Installing terminal tools..."
log() {
printf '[terminal-tools] %s\n' "$1"
}
# Install superfile (terminal file manager)
echo "Installing superfile..."
if ! command -v spf >/dev/null 2>&1; then
curl -sL https://superfile.netlify.app/install.sh | bash
# Add to PATH if not already there
if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
export PATH="$HOME/.local/bin:$PATH"
check_tool() {
local name="$1"
local binary="$2"
local version_spec="$3"
if command -v "$binary" >/dev/null 2>&1; then
local resolved
resolved=$(command -v "$binary")
local version_output
if [[ -n "$version_spec" ]]; then
# shellcheck disable=SC2206 # intentional word splitting for flags
local version_args=( $version_spec )
if version_output=$("$binary" "${version_args[@]}" 2>/dev/null | head -n 1); then
log "$name available ($version_output)"
else
log "$name available at $resolved"
fi
else
log "$name available at $resolved"
fi
else
echo "superfile already installed"
else
log "$name missing from PATH"
MISSING_TOOLS=1
fi
}
log "Validating terminal tooling baked into the image"
MISSING_TOOLS=0
check_tool "Superfile" "spf" "--version"
check_tool "lazygit" "lazygit" "--version"
check_tool "lazydocker" "lazydocker" "--version"
check_tool "btop" "btop" "--version"
if [[ "$MISSING_TOOLS" -eq 1 ]]; then
log "One or more tools are missing; ensure the base image installs them."
exit 1
fi
# Install lazygit (terminal git manager)
echo "Installing lazygit..."
if ! command -v lazygit >/dev/null 2>&1; then
LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | grep -Po '"tag_name": "v\K[^"]*')
curl -Lo lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz"
tar xf lazygit.tar.gz lazygit
# Install to user's local bin directory to avoid sudo
mkdir -p $HOME/.local/bin
cp lazygit $HOME/.local/bin/
chmod +x $HOME/.local/bin/lazygit
rm lazygit lazygit.tar.gz
else
echo "lazygit already installed"
fi
# Install lazydocker (terminal docker manager)
echo "Installing lazydocker..."
if ! command -v lazydocker >/dev/null 2>&1; then
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash
else
echo "lazydocker already installed"
fi
# Install btop (better htop alternative for system monitoring)
echo "Installing btop..."
if ! command -v btop >/dev/null 2>&1; then
echo "Installing btop from GitHub releases..."
BTOP_VERSION=$(curl -s "https://api.github.com/repos/aristocratos/btop/releases/latest" | grep -Po '"tag_name": "v\K[^"]*')
curl -Lo btop.tbz "https://github.com/aristocratos/btop/releases/latest/download/btop-x86_64-linux-musl.tbz"
tar -xjf btop.tbz
# Install to user's local bin directory to avoid sudo
mkdir -p $HOME/.local/bin
cp btop/bin/btop $HOME/.local/bin/
chmod +x $HOME/.local/bin/btop
rm -rf btop btop.tbz
else
echo "btop already installed"
fi
echo "Terminal tools installation completed successfully!"
log "All terminal tools present"

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[windsurf-setup] %s\n' "$1"
}
SETTINGS_DIR="${HOME:-/home/coder}/.config/windsurf"
mkdir -p "$SETTINGS_DIR"
log "Windsurf configuration directory ensured at $SETTINGS_DIR"

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[workspace-setup] %s\n' "$1"
}
CODE_SERVER_MACHINE_DIR="${HOME:-/home/coder}/.local/share/code-server/Machine"
mkdir -p "$CODE_SERVER_MACHINE_DIR"
MACHINE_SETTINGS="$CODE_SERVER_MACHINE_DIR/settings.json"
if [ ! -s "$MACHINE_SETTINGS" ]; then
cat >"$MACHINE_SETTINGS" <<'JSON'
{}
JSON
log "Created machine settings file at $MACHINE_SETTINGS"
else
log "Machine settings already present at $MACHINE_SETTINGS"
fi
log "Workspace setup checks complete."

View File

@@ -202,7 +202,9 @@ resource "docker_container" "pgadmin" {
"PGADMIN_DEFAULT_PASSWORD=${var.pgadmin_password}",
"PGADMIN_CONFIG_SERVER_MODE=False",
"PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False",
"PGADMIN_LISTEN_PORT=5050"
"PGADMIN_LISTEN_PORT=5050",
"PGADMIN_CONFIG_GLOBALLY_DELIVERABLE=False",
"PGADMIN_CONFIG_ALLOW_SPECIAL_EMAIL_DOMAINS=['local']"
]
networks_advanced {
@@ -215,7 +217,7 @@ resource "docker_container" "pgadmin" {
}
healthcheck {
test = ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:5050/misc/ping || exit 1"]
test = ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:5050/misc/ping', timeout=5)\" || exit 1"]
interval = "30s"
timeout = "10s"
retries = 3

View File

@@ -1,4 +1,4 @@
devcontainer_image = "git.lab/vasceannie/code-tools:golden-devcontainer"
devcontainer_image = "git.lab/vasceannie/golden-image:latest"
workspace_memory_limit = 8192
postgres_password = "devpassword"
redis_password = "devpassword"

View File

@@ -13,7 +13,7 @@ variable "docker_socket" {
variable "devcontainer_image" {
description = "Container image used for the main workspace."
type = string
default = "git.lab/vasceannie/code-tools:golden-devcontainer"
default = "git.lab/vasceannie/golden-image:latest"
}
variable "workspace_memory_limit" {
@@ -135,4 +135,3 @@ variable "github_pat" {
default = ""
sensitive = true
}

View File

@@ -1,3 +1,17 @@
locals {
workspace_agent_scripts = {
"workspace-setup.sh" = file("${path.module}/scripts/workspace-setup.sh")
"dev-tools.sh" = file("${path.module}/scripts/dev-tools.sh")
"terminal-tools.sh" = file("${path.module}/scripts/terminal-tools.sh")
"git-hooks.sh" = file("${path.module}/scripts/git-hooks.sh")
"marimo-setup.sh" = file("${path.module}/scripts/marimo-setup.sh")
"claude-install.sh" = file("${path.module}/scripts/claude-install.sh")
"codex-setup.sh" = file("${path.module}/scripts/codex-setup.sh")
"cursor-setup.sh" = file("${path.module}/scripts/cursor-setup.sh")
"windsurf-setup.sh" = file("${path.module}/scripts/windsurf-setup.sh")
}
}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
@@ -5,15 +19,20 @@ resource "coder_agent" "main" {
env = merge(
{
"GIT_AUTHOR_NAME" = local.git_author_name
"GIT_AUTHOR_EMAIL" = local.git_author_email
"CODER_WORKSPACE_ID" = local.workspace_id
"CODER_WORKSPACE_REPO" = local.project_repo_url
"ENABLE_MARIMO" = tostring(local.marimo_enabled)
"ENABLE_SERVICES" = tostring(local.services_enabled)
"CODER_AGENT_BLOCK_FILE_TRANSFER" = var.block_file_transfer ? "1" : ""
"GEM_HOME" = "/home/coder/.gem"
"GEM_PATH" = "/home/coder/.gem"
"GIT_AUTHOR_NAME" = local.git_author_name
"GIT_AUTHOR_EMAIL" = local.git_author_email
"CODER_WORKSPACE_ID" = local.workspace_id
"CODER_WORKSPACE_REPO" = local.project_repo_url
"ENABLE_MARIMO" = tostring(local.marimo_enabled)
"ENABLE_SERVICES" = tostring(local.services_enabled)
"CODER_AGENT_BLOCK_FILE_TRANSFER" = var.block_file_transfer ? "1" : ""
"GEM_HOME" = "/home/coder/.gem"
"GEM_PATH" = "/home/coder/.gem"
"rvm_silence_path_mismatch_check_flag" = "1"
"RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG" = "1"
"rvmsudo_secure_path" = "1"
"RVMSUDO_SECURE_PATH" = "1"
"NVM_DIR" = "/usr/local/share/nvm"
},
local.gitea_pat != "" ? { "GITEA_PAT" = local.gitea_pat } : {},
local.github_pat != "" ? { "GITHUB_PAT" = local.github_pat } : {}
@@ -38,8 +57,23 @@ resource "coder_agent" "main" {
echo "docker group not found; skipping docker socket setup"
fi
# Switch to coder user for service startup
sudo -u coder bash << 'CODER_SETUP'
# Switch to coder user for service startup with gem environment pre-set
sudo --preserve-env=CODER_WORKSPACE_ID,CODER_WORKSPACE_REPO,GITEA_PAT,GITHUB_PAT -u coder \
env -i \
HOME=/home/coder \
PATH="/home/coder/.venv/bin:/home/coder/.local/bin:/home/coder/bin:/home/coder/.cargo/bin:/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin" \
GEM_HOME=/home/coder/.gem \
GEM_PATH=/home/coder/.gem \
NVM_DIR=/usr/local/share/nvm \
CODER_WORKSPACE_ID="$${CODER_WORKSPACE_ID-}" \
CODER_WORKSPACE_REPO="$${CODER_WORKSPACE_REPO-}" \
GITEA_PAT="$${GITEA_PAT-}" \
GITHUB_PAT="$${GITHUB_PAT-}" \
rvm_silence_path_mismatch_check_flag=1 \
RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG=1 \
rvmsudo_secure_path=1 \
RVMSUDO_SECURE_PATH=1 \
bash <<'CODER_SETUP'
set -euo pipefail
# Consistent RubyGems environment for every shell
@@ -72,6 +106,8 @@ ensure_shell_env_block() {
# >>> coder gem environment >>>
export GEM_HOME="$HOME/.gem"
export GEM_PATH="$GEM_HOME"
export rvm_silence_path_mismatch_check_flag=1
export RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG=1
if [ -d "$HOME/.gem/bin" ]; then
case ":$PATH:" in
*":$HOME/.gem/bin:"*) ;;
@@ -88,6 +124,25 @@ fi
EOF
}
current_nvm=$(printenv NVM_DIR 2>/dev/null || true)
if [ -z "$current_nvm" ]; then
NVM_DIR=/usr/local/share/nvm
else
NVM_DIR="$current_nvm"
fi
export NVM_DIR
if [ -d "$NVM_DIR" ]; then
if [ -s "$NVM_DIR/nvm.sh" ]; then
# shellcheck disable=SC1090,SC1091
. "$NVM_DIR/nvm.sh" >/dev/null 2>&1 || true
fi
shopt -s nullglob
for node_bin in "$NVM_DIR"/versions/node/*/bin; do
PATH="$node_bin:$PATH"
done
shopt -u nullglob
fi
for shell_rc in ~/.bashrc ~/.profile ~/.zprofile ~/.zshrc ~/.zshenv; do
ensure_shell_env_block "$shell_rc"
done
@@ -98,7 +153,13 @@ workspace_has_content=false
if [ -n "$(find /workspaces -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'lost+found' -print -quit 2>/dev/null)" ]; then
workspace_has_content=true
fi
if [ -n "$${CODER_WORKSPACE_REPO:-}" ]; then
repo_value=$(printenv CODER_WORKSPACE_REPO 2>/dev/null || true)
repo_display="<empty>"
if [ -n "$repo_value" ]; then
repo_display="$repo_value"
fi
echo "CODER_WORKSPACE_REPO=$repo_display"
if [ -n "$repo_value" ]; then
if [ -d /workspaces/.git ]; then
echo "Git repository already present in /workspaces"
else
@@ -127,228 +188,68 @@ mkdir -p "$HOME/.config/fish/conf.d"
cat <<'EOF' > "$HOME/.config/fish/conf.d/gem_env.fish"
set -gx GEM_HOME $HOME/.gem
set -gx GEM_PATH $HOME/.gem
set -gx rvm_silence_path_mismatch_check_flag 1
set -gx RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG 1
if not contains $HOME/.gem/bin $PATH
set -gx PATH $HOME/.gem/bin $PATH
end
EOF
# Install terminal tools if not already installed
echo "Installing terminal tools..."
# Install superfile (terminal file manager)
if ! command -v spf >/dev/null 2>&1; then
echo "Installing superfile..."
curl -sL https://superfile.netlify.app/install.sh | bash
# Add to PATH if not already there
if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
export PATH="$HOME/.local/bin:$PATH"
fi
else
echo "superfile already installed"
fi
# Materialize bundled scripts for tooling, workspace setup, and optional AI helpers
SCRIPT_ROOT="$HOME/.coder/scripts"
mkdir -p "$SCRIPT_ROOT"
# Install lazygit (terminal git manager)
if ! command -v lazygit >/dev/null 2>&1; then
echo "Installing lazygit..."
LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | grep -Po '"tag_name": "v\K[^"]*')
curl -Lo lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_$${LAZYGIT_VERSION}_Linux_x86_64.tar.gz"
tar xf lazygit.tar.gz lazygit
# Install to user's local bin directory to avoid sudo
mkdir -p $HOME/.local/bin
cp lazygit $HOME/.local/bin/
chmod +x $HOME/.local/bin/lazygit
rm lazygit lazygit.tar.gz
else
echo "lazygit already installed"
fi
# Install lazydocker (terminal docker manager)
if ! command -v lazydocker >/dev/null 2>&1; then
echo "Installing lazydocker..."
# Install lazydocker to user's local bin directory to avoid sudo
LAZYDOCKER_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazydocker/releases/latest" | grep -Po '"tag_name": "v\K[^"]*')
curl -Lo lazydocker.tar.gz "https://github.com/jesseduffield/lazydocker/releases/latest/download/lazydocker_$${LAZYDOCKER_VERSION}_Linux_x86_64.tar.gz"
tar xf lazydocker.tar.gz lazydocker
mkdir -p $HOME/.local/bin
cp lazydocker $HOME/.local/bin/
chmod +x $HOME/.local/bin/lazydocker
rm lazydocker lazydocker.tar.gz
else
echo "lazydocker already installed"
fi
# Install btop (better htop alternative for system monitoring)
if ! command -v btop >/dev/null 2>&1; then
echo "Installing btop from GitHub releases..."
BTOP_VERSION=$(curl -s "https://api.github.com/repos/aristocratos/btop/releases/latest" | grep -Po '"tag_name": "v\K[^"]*')
curl -Lo btop.tbz "https://github.com/aristocratos/btop/releases/latest/download/btop-x86_64-linux-musl.tbz"
tar -xjf btop.tbz
# Install to user's local bin directory to avoid sudo
mkdir -p $HOME/.local/bin
cp btop/bin/btop $HOME/.local/bin/
chmod +x $HOME/.local/bin/btop
rm -rf btop btop.tbz
else
echo "btop already installed"
fi
echo "Terminal tools installation completed!"
# Install and start code-server
echo "Setting up code-server..."
if [ ! -f /tmp/code-server/bin/code-server ]; then
echo "Installing code-server..."
curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server
if [ $? -ne 0 ]; then
echo "Failed to install code-server"
exit 1
install_embedded_script() {
local name="$1"
local encoded="$2"
if [ -z "$encoded" ]; then
echo "Skipping $name; embedded content missing"
return
fi
fi
printf '%s' "$encoded" | base64 -d > "$SCRIPT_ROOT/$name"
chmod +x "$SCRIPT_ROOT/$name"
}
# Verify installation
if [ ! -f /tmp/code-server/bin/code-server ]; then
echo "ERROR: code-server binary not found after installation"
exit 1
fi
%{for script_name, script_content in local.workspace_agent_scripts}
install_embedded_script "${script_name}" "${base64encode(script_content)}"
%{endfor}
# Create config directory
mkdir -p $HOME/.config/code-server
cat > $HOME/.config/code-server/config.yaml << 'CONFIG'
bind-addr: 127.0.0.1:13337
auth: none
cert: false
CONFIG
# Run bundled scripts after ensuring they exist on disk
run_script() {
local label="$1"
local path="$2"
if [ -x "$path" ]; then
echo "Running $label..."
"$path"
elif [ -f "$path" ]; then
echo "Running $label from $path..."
bash "$path"
else
echo "Skipping $label; script not found at $path"
fi
}
echo "Starting code-server..."
/tmp/code-server/bin/code-server --config $HOME/.config/code-server/config.yaml /workspaces > /tmp/code-server.log 2>&1 &
run_script "workspace setup" "$SCRIPT_ROOT/workspace-setup.sh"
run_script "developer tools check" "$SCRIPT_ROOT/dev-tools.sh"
run_script "terminal tools verifier" "$SCRIPT_ROOT/terminal-tools.sh"
run_script "git hooks configuration" "$SCRIPT_ROOT/git-hooks.sh"
# Wait a moment and check if it started
sleep 2
if pgrep -f "code-server" > /dev/null; then
echo "✅ code-server started successfully"
else
echo "❌ code-server failed to start, check /tmp/code-server.log"
fi
# Install and start Marimo if enabled
if [ "${tostring(local.marimo_enabled)}" = "true" ]; then
echo "Installing latest Marimo with uv..."
# Use uv to create venv and install marimo
export HOME=/home/coder
export USER=coder
cd /home/coder
# Install Python 3.12 if not available
if ! command -v python3.12 >/dev/null 2>&1; then
echo "Installing Python 3.12..."
apt-get update -qq
apt-get install -y software-properties-common
add-apt-repository ppa:deadsnakes/ppa -y
apt-get update -qq
apt-get install -y python3.12 python3.12-venv python3.12-pip
fi
# Find uv binary
UV_BIN=""
if command -v uv >/dev/null 2>&1; then
UV_BIN="uv"
elif [ -f "/home/coder/.local/bin/uv" ]; then
UV_BIN="/home/coder/.local/bin/uv"
elif [ -f "/usr/local/bin/uv" ]; then
UV_BIN="/usr/local/bin/uv"
fi
if [ -n "$UV_BIN" ]; then
# Create virtual environment if it doesn't exist
if [ ! -d "/home/coder/.venv" ]; then
$UV_BIN venv -p python3.12 /home/coder/workspaces/.venv
fi
# Install marimo in the venv
mkdir -p /home/coder/workspaces && cd /home/coder/workspaces && $UV_BIN pip install --upgrade marimo
else
echo "uv not found, falling back to pip"
pip install --user --upgrade marimo
fi
if [ $? -ne 0 ]; then
echo "Failed to install Marimo"
exit 1
fi
# Ensure .local/bin is in PATH
export PATH="$HOME/.local/bin:$PATH"
# Add to bashrc for persistence
if ! grep -q 'export PATH="$HOME/.local/bin:$PATH"' ~/.bashrc; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
fi
# Verify installation
if [ ! -f "$HOME/.local/bin/marimo" ]; then
echo "ERROR: marimo binary not found after installation"
exit 1
fi
# Create a simple marimo notebook directory structure
mkdir -p ~/workspaces/notebooks
# Create a basic marimo app if none exists
if [ ! -f "~/workspaces/notebooks/welcome.py" ]; then
cat > ~/workspaces/notebooks/welcome.py << 'MARIMO_APP'
import marimo
__generated_with = "0.16.0"
app = marimo.App()
@app.cell
def __():
import marimo as mo
return mo,
@app.cell
def __(mo):
mo.md("# Welcome to Marimo!")
return
@app.cell
def __(mo):
mo.md("This is your interactive notebook environment.")
return
if __name__ == "__main__":
app.run()
MARIMO_APP
fi
echo "Starting Marimo..."
# Kill any existing marimo processes first
pkill -f marimo || true
# Start marimo with proper environment and activate venv
export HOME=/home/coder
export USER=coder
export PATH="/home/coder/.venv/bin:/home/coder/.local/bin:$PATH"
cd /home/coder
# Activate virtual environment if it exists
if [ -f "/home/coder/.venv/bin/activate" ]; then
source /home/coder/.venv/bin/activate
fi
nohup marimo edit --headless --host 0.0.0.0 --port 8888 > /tmp/marimo.log 2>&1 &
# Wait a moment and check if it started
sleep 3
if pgrep -f "marimo" > /dev/null; then
echo "✅ Marimo started successfully"
else
echo "❌ Marimo failed to start, check /tmp/marimo.log"
cat /tmp/marimo.log
fi
run_script "Marimo setup" "$SCRIPT_ROOT/marimo-setup.sh"
else
echo "Marimo disabled (enable_marimo = ${tostring(local.marimo_enabled)})"
fi
if [ "${tostring(local.ai_enabled)}" = "true" ]; then
run_script "Claude CLI setup" "$SCRIPT_ROOT/claude-install.sh"
run_script "Codex CLI setup" "$SCRIPT_ROOT/codex-setup.sh"
run_script "Cursor setup" "$SCRIPT_ROOT/cursor-setup.sh"
run_script "Windsurf setup" "$SCRIPT_ROOT/windsurf-setup.sh"
else
echo "AI tooling disabled (enable_ai_tools = ${tostring(local.ai_enabled)})"
fi
echo "Services started successfully"
CODER_SETUP
@@ -508,7 +409,21 @@ CODER_SETUP
metadata {
display_name = "Qdrant"
key = "6_qdrant"
script = local.services_enabled ? format("wget --no-verbose --tries=1 --spider http://qdrant-%s:6333/health 2>/dev/null && echo healthy || echo down", local.workspace_id) : "echo 'disabled'"
script = <<-EOT
if [ "${tostring(local.services_enabled)}" = "true" ]; then
target="qdrant-${local.workspace_id}"
if command -v curl >/dev/null 2>&1; then
status=$(curl -s -o /dev/null -w "%%{http_code}" --connect-timeout 3 "http://$target:6333/healthz")
if [ "$status" = "200" ]; then echo healthy; else echo "unhealthy($status)"; fi
elif command -v wget >/dev/null 2>&1; then
if wget -qO- --timeout=3 "http://$target:6333/healthz" >/dev/null; then echo healthy; else echo down; fi
else
echo unknown
fi
else
echo 'disabled'
fi
EOT
interval = 60
timeout = 5
}
@@ -571,7 +486,7 @@ resource "docker_container" "workspace" {
}
working_dir = "/workspaces"
command = ["/bin/bash", "-c", <<-EOT
command = ["/bin/bash", "-c", <<-EOT
# Run init script as root to handle permissions
${coder_agent.main.init_script}
@@ -598,6 +513,20 @@ resource "docker_container" "workspace" {
]
}
module "code_server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.3.1"
agent_id = coder_agent.main.id
install_prefix = "/opt/code-server"
offline = true
port = 13337
folder = "/workspaces"
display_name = "code-server"
group = "Web IDEs"
order = 10
}
module "cursor_desktop" {
count = data.coder_workspace.me.start_count > 0 && data.coder_parameter.enable_ai_tools.value ? 1 : 0
source = "registry.coder.com/coder/cursor/coder"
@@ -760,4 +689,3 @@ module "pycharm_desktop" {
# esac
# EOT
# }