stuff
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
# code-tools
|
||||
|
||||
cloudflared.exe service install eyJhIjoiZjIxNTFlNzZiYzQ3NmZkMWRhNWMzNmRkMDBkZWIzYzEiLCJ0IjoiMDU3ODVmZTQtMGRlOS00OTQ1LThhMWMtOTMzZWRiYmQzOTc0IiwicyI6IlptRXpOekUyTWpJdE5EaGlOaTAwTmpVeExUaGxNall0Tnpjd01HRTJNVEJqTkRsaSJ9
|
||||
270
cert-mgmt/README.md
Normal file
270
cert-mgmt/README.md
Normal 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)
|
||||
13
cert-mgmt/inventory/network_inventory.yaml
Normal file
13
cert-mgmt/inventory/network_inventory.yaml
Normal 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:
|
||||
252
cert-mgmt/scripts/cert_deployment_orchestrator.sh
Normal file
252
cert-mgmt/scripts/cert_deployment_orchestrator.sh
Normal 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
|
||||
79
cert-mgmt/scripts/create_inventory.sh
Normal file
79
cert-mgmt/scripts/create_inventory.sh
Normal 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"
|
||||
205
cert-mgmt/scripts/deploy_to_containers.sh
Normal file
205
cert-mgmt/scripts/deploy_to_containers.sh
Normal 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
|
||||
130
cert-mgmt/scripts/deploy_to_hosts.sh
Normal file
130
cert-mgmt/scripts/deploy_to_hosts.sh
Normal 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
|
||||
89
cert-mgmt/scripts/generate_certificate.sh
Normal file
89
cert-mgmt/scripts/generate_certificate.sh
Normal 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!"
|
||||
117
cert-mgmt/test_validation.sh
Normal file
117
cert-mgmt/test_validation.sh
Normal 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
37
terraform/AGENTS.md
Normal 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.citeturn0search11
|
||||
- `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.citeturn0search5
|
||||
|
||||
## 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`.citeturn0search5
|
||||
|
||||
## 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.citeturn0search11
|
||||
- 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.citeturn0search2
|
||||
@@ -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` |
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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)}",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
17
terraform/scripts/claude-install.sh
Executable file
17
terraform/scripts/claude-install.sh
Executable 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
|
||||
17
terraform/scripts/codex-setup.sh
Executable file
17
terraform/scripts/codex-setup.sh
Executable 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
|
||||
10
terraform/scripts/cursor-setup.sh
Executable file
10
terraform/scripts/cursor-setup.sh
Executable 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
121
terraform/scripts/dev-tools.sh
Executable 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
106
terraform/scripts/marimo-setup.sh
Executable 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"
|
||||
@@ -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
90
terraform/scripts/terminal-tools.sh
Normal file → Executable 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"
|
||||
|
||||
10
terraform/scripts/windsurf-setup.sh
Executable file
10
terraform/scripts/windsurf-setup.sh
Executable 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"
|
||||
21
terraform/scripts/workspace-setup.sh
Executable file
21
terraform/scripts/workspace-setup.sh
Executable 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."
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
# }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user