Compare commits
5 Commits
docs/add-f
...
feature/k8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f41e08bc6 | ||
|
|
2c82099bdd | ||
|
|
dd967ff223 | ||
|
|
569e84562e | ||
|
|
a51398a336 |
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Pre-push hook - Runs tests before pushing
|
||||
# Pre-push hook - Runs tests before pushing (supports both Sail and K8s)
|
||||
# Can be skipped with: git push --no-verify
|
||||
#
|
||||
|
||||
@@ -8,11 +8,48 @@ echo "🧪 Running tests before push..."
|
||||
echo " (Use 'git push --no-verify' to skip)"
|
||||
echo ""
|
||||
|
||||
# Run tests
|
||||
./vendor/bin/sail artisan test --parallel
|
||||
# Detect which environment is running
|
||||
SAIL_RUNNING=false
|
||||
K8S_RUNNING=false
|
||||
|
||||
# Check exit code
|
||||
if [ $? -ne 0 ]; then
|
||||
# Check if Sail is running
|
||||
if docker ps --format '{{.Names}}' | grep -q "sail" 2>/dev/null; then
|
||||
SAIL_RUNNING=true
|
||||
echo "📦 Detected Sail environment"
|
||||
fi
|
||||
|
||||
# Check if k8s namespace exists for this worktree
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
K8S_NS=$(echo "$BRANCH" | sed 's/feature\//feat-/' | sed 's/bugfix\//fix-/' | sed 's/\//-/g')
|
||||
|
||||
if kubectl get namespace "$K8S_NS" >/dev/null 2>&1; then
|
||||
K8S_RUNNING=true
|
||||
echo "☸️ Detected K8s environment (namespace: $K8S_NS)"
|
||||
fi
|
||||
|
||||
# Run tests in appropriate environment
|
||||
if [ "$SAIL_RUNNING" = true ]; then
|
||||
./vendor/bin/sail artisan test --parallel
|
||||
TEST_EXIT_CODE=$?
|
||||
elif [ "$K8S_RUNNING" = true ]; then
|
||||
echo " Running tests in k8s pod..."
|
||||
kubectl -n "$K8S_NS" exec deploy/web -- php artisan test
|
||||
TEST_EXIT_CODE=$?
|
||||
else
|
||||
echo "⚠️ No environment running (Sail or K8s)"
|
||||
echo " Skipping tests - please run tests manually"
|
||||
echo ""
|
||||
read -p "Continue push anyway? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
if [ ! "$REPLY" = "y" ] && [ ! "$REPLY" = "Y" ]; then
|
||||
echo "Push aborted"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check test results
|
||||
if [ $TEST_EXIT_CODE -ne 0 ]; then
|
||||
echo ""
|
||||
echo "❌ Tests failed!"
|
||||
echo ""
|
||||
|
||||
94
Makefile
94
Makefile
@@ -1,8 +1,23 @@
|
||||
.PHONY: help dev dev-down dev-build dev-shell dev-logs dev-vite prod-build prod-up prod-down prod-logs prod-shell prod-vite prod-test prod-test-build prod-test-up prod-test-down prod-test-logs prod-test-shell prod-test-status prod-test-clean migrate test clean install
|
||||
.PHONY: help dev dev-down dev-build dev-shell dev-logs dev-vite k-dev k-down k-logs k-shell k-artisan k-composer k-vite k-status prod-build prod-up prod-down prod-logs prod-shell prod-vite prod-test prod-test-build prod-test-up prod-test-down prod-test-logs prod-test-shell prod-test-status prod-test-clean migrate test clean install
|
||||
|
||||
# Default target
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# ==================== K8s Variables ====================
|
||||
# Auto-detect worktree name from current directory
|
||||
WORKTREE_NAME := $(shell basename $(CURDIR))
|
||||
# Generate namespace from branch name (feat-branch-name)
|
||||
CURRENT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
|
||||
K8S_NS := $(shell echo "$(CURRENT_BRANCH)" | sed 's/feature\//feat-/' | sed 's/bugfix\//fix-/' | sed 's/\//-/g')
|
||||
# Generate sanitized branch name for database
|
||||
SANITIZED_BRANCH := $(shell echo "$(CURRENT_BRANCH)" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||
# Generate host from branch
|
||||
K8S_HOST := $(shell echo "$(CURRENT_BRANCH)" | sed 's/feature\///' | sed 's/bugfix\///' | sed 's/\//-/g').cannabrands.test
|
||||
# Read database credentials from .env
|
||||
DB_USERNAME := $(shell grep '^DB_USERNAME=' .env 2>/dev/null | cut -d '=' -f2)
|
||||
DB_PASSWORD := $(shell grep '^DB_PASSWORD=' .env 2>/dev/null | cut -d '=' -f2)
|
||||
DB_DATABASE := $(shell grep '^DB_DATABASE=' .env 2>/dev/null | cut -d '=' -f2)
|
||||
|
||||
# ==================== Local Development (Sail) ====================
|
||||
dev: ## Start local development environment with Sail
|
||||
./vendor/bin/sail up -d
|
||||
@@ -31,6 +46,81 @@ dev-composer: ## Run composer command (usage: make dev-composer CMD="install")
|
||||
dev-vite: ## Start Vite dev server (run after 'make dev')
|
||||
./vendor/bin/sail npm run dev
|
||||
|
||||
# ==================== K8s Local Development ====================
|
||||
k-dev: ## Start k8s local environment (like Sail, but with namespace isolation)
|
||||
@echo "🚀 Starting k8s environment for: $(WORKTREE_NAME)"
|
||||
@echo " Namespace: $(K8S_NS)"
|
||||
@echo " Branch: $(CURRENT_BRANCH)"
|
||||
@echo " URL: http://$(K8S_HOST)"
|
||||
@echo ""
|
||||
@# Create namespace
|
||||
@kubectl create ns $(K8S_NS) --dry-run=client -o yaml | kubectl apply -f -
|
||||
@# Create secrets from .env
|
||||
@kubectl -n $(K8S_NS) delete secret app-env --ignore-not-found
|
||||
@kubectl -n $(K8S_NS) create secret generic app-env --from-env-file=.env
|
||||
@# Create PostgreSQL auth secret (using credentials from .env)
|
||||
@kubectl -n $(K8S_NS) create secret generic pg-auth --dry-run=client -o yaml \
|
||||
--from-literal=POSTGRES_DB=$(DB_DATABASE) \
|
||||
--from-literal=POSTGRES_USER=$(DB_USERNAME) \
|
||||
--from-literal=POSTGRES_PASSWORD=$(DB_PASSWORD) | kubectl apply -f -
|
||||
@# Deploy PostgreSQL
|
||||
@export NS=$(K8S_NS) PG_DB=$(DB_DATABASE) PG_USER=$(DB_USERNAME) PG_PASS=$(DB_PASSWORD) && \
|
||||
envsubst < k8s/local/postgres.yaml | kubectl apply -f -
|
||||
@# Deploy Redis
|
||||
@export NS=$(K8S_NS) && \
|
||||
envsubst < k8s/local/redis.yaml | kubectl apply -f -
|
||||
@# Wait for DB
|
||||
@echo "⏳ Waiting for PostgreSQL..."
|
||||
@kubectl -n $(K8S_NS) wait --for=condition=ready pod -l app=postgres --timeout=60s
|
||||
@# Deploy app (with code volume mounted)
|
||||
@export NS=$(K8S_NS) WORKTREE_NAME=$(WORKTREE_NAME) K8S_HOST=$(K8S_HOST) && \
|
||||
envsubst < k8s/local/deployment.yaml | kubectl apply -f -
|
||||
@# Create service + ingress
|
||||
@export NS=$(K8S_NS) K8S_HOST=$(K8S_HOST) && \
|
||||
envsubst < k8s/local/service.yaml | kubectl apply -f - && \
|
||||
envsubst < k8s/local/ingress.yaml | kubectl apply -f -
|
||||
@echo ""
|
||||
@echo "✅ Ready! Visit: http://$(K8S_HOST)"
|
||||
@echo ""
|
||||
@echo "💡 Your code is volume-mounted - changes are instant!"
|
||||
@echo " Edit files → refresh browser → see changes"
|
||||
@echo ""
|
||||
@echo "📝 Useful commands:"
|
||||
@echo " make k-logs # View app logs"
|
||||
@echo " make k-shell # Open shell in pod"
|
||||
@echo " make k-vite # Start Vite dev server"
|
||||
|
||||
k-down: ## Stop k8s environment
|
||||
@echo "🗑 Removing namespace: $(K8S_NS)"
|
||||
@kubectl delete ns $(K8S_NS) --ignore-not-found
|
||||
@echo "✅ Cleaned up"
|
||||
|
||||
k-logs: ## View app logs
|
||||
@kubectl -n $(K8S_NS) logs -f deploy/web --all-containers=true
|
||||
|
||||
k-shell: ## Shell into app container
|
||||
@kubectl -n $(K8S_NS) exec -it deploy/web -- /bin/bash
|
||||
|
||||
k-artisan: ## Run artisan command (usage: make k-artisan CMD="migrate")
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- php artisan $(CMD)
|
||||
|
||||
k-composer: ## Run composer (usage: make k-composer CMD="install")
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- composer $(CMD)
|
||||
|
||||
k-vite: ## Run Vite dev server in k8s pod
|
||||
@echo "🎨 Starting Vite dev server in pod..."
|
||||
@echo " Access at: http://vite.$(K8S_HOST)"
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- npm run dev
|
||||
|
||||
k-test: ## Run tests in k8s pod
|
||||
@echo "🧪 Running tests in k8s pod..."
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- php artisan test
|
||||
|
||||
k-status: ## Show k8s environment status
|
||||
@echo "📊 Status for namespace: $(K8S_NS)"
|
||||
@echo ""
|
||||
@kubectl -n $(K8S_NS) get pods,svc,ingress
|
||||
|
||||
# ==================== Production ====================
|
||||
prod-build: ## Build production Docker image
|
||||
docker build -t cannabrands/app:latest -f Dockerfile .
|
||||
@@ -136,6 +226,8 @@ help: ## Show this help message
|
||||
@echo "\n📦 CannaBrands Docker Commands\n"
|
||||
@echo "Local Development (Sail):"
|
||||
@grep -E '^dev.*:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-25s\033[0m %s\n", $$1, $$2}'
|
||||
@echo "\nK8s Local Development:"
|
||||
@grep -E '^k-.*:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[35m%-25s\033[0m %s\n", $$1, $$2}'
|
||||
@echo "\nProduction Testing (Local):"
|
||||
@grep -E '^prod-test.*:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[33m%-25s\033[0m %s\n", $$1, $$2}'
|
||||
@echo "\nProduction (K8s/Deployment):"
|
||||
|
||||
340
NEXT_STEPS.md
Normal file
340
NEXT_STEPS.md
Normal file
@@ -0,0 +1,340 @@
|
||||
# Next Steps: QR Code Feature & K8s Local Dev Setup
|
||||
|
||||
This document outlines the next steps for both features that were separated into different git worktrees.
|
||||
|
||||
---
|
||||
|
||||
## Summary of Work Done
|
||||
|
||||
### ✅ QR Code Feature (feature/batch-tracking-coa-qr branch)
|
||||
**Location:** `.worktrees/labs-batch-qr-codes/`
|
||||
**Status:** Committed and ready for testing/PR
|
||||
|
||||
**Changes:**
|
||||
- QR code generation endpoints in BatchController
|
||||
- Filament actions for QR code management
|
||||
- UI for QR code display in batch views and public COA
|
||||
- Comprehensive test suite
|
||||
- Routes for single and bulk operations
|
||||
|
||||
### ✅ K8s Local Dev Setup (feature/k8s-local-dev branch)
|
||||
**Location:** `.worktrees/infra-k8s-local-dev/`
|
||||
**Status:** Committed and ready for setup/testing
|
||||
|
||||
**Changes:**
|
||||
- K8s manifests for local development (k8s/local/)
|
||||
- Makefile targets with `k-` prefix
|
||||
- Complete documentation (docs/K8S_*.md)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### For QR Code Feature
|
||||
|
||||
**1. Test the Implementation**
|
||||
|
||||
```bash
|
||||
cd /Users/jon/projects/cannabrands/cannabrands_new/.worktrees/labs-batch-qr-codes
|
||||
|
||||
# Start services if not already running
|
||||
docker-compose up -d pgsql redis
|
||||
|
||||
# Run migrations (via Sail if Redis extension issue persists)
|
||||
./vendor/bin/sail up -d
|
||||
./vendor/bin/sail artisan migrate
|
||||
|
||||
# Run tests
|
||||
./vendor/bin/sail artisan test --filter=QrCodeGenerationTest
|
||||
|
||||
# Or run all tests
|
||||
./vendor/bin/sail artisan test
|
||||
```
|
||||
|
||||
**2. Manual Testing**
|
||||
|
||||
```bash
|
||||
# Start Sail environment
|
||||
./vendor/bin/sail up -d
|
||||
|
||||
# Access app
|
||||
open http://localhost
|
||||
|
||||
# Test:
|
||||
# - Navigate to a batch in Filament
|
||||
# - Generate QR code
|
||||
# - Download QR code
|
||||
# - View public COA (should show QR)
|
||||
# - Test bulk generation
|
||||
```
|
||||
|
||||
**3. Create Pull Request**
|
||||
|
||||
```bash
|
||||
# Push branch to remote
|
||||
git push origin feature/batch-tracking-coa-qr
|
||||
|
||||
# Create PR via GitHub CLI or web interface
|
||||
gh pr create --title "feat: Add QR code generation for batches" \
|
||||
--body "$(cat <<EOF
|
||||
## Summary
|
||||
Implements QR code generation for batches with download and bulk operations.
|
||||
|
||||
## Features
|
||||
- Single and bulk QR code generation
|
||||
- Download functionality
|
||||
- Public COA display
|
||||
- Business ownership validation
|
||||
- Comprehensive test suite
|
||||
|
||||
## Testing
|
||||
- [ ] All tests pass
|
||||
- [ ] Manual testing completed
|
||||
- [ ] QR codes generate correctly
|
||||
- [ ] Download works
|
||||
- [ ] Public COA displays QR
|
||||
|
||||
## Screenshots
|
||||
[Add screenshots of QR generation and COA display]
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### For K8s Local Dev Setup
|
||||
|
||||
**1. One-Time K3d Setup**
|
||||
|
||||
Follow the complete guide in `docs/K8S_LOCAL_SETUP.md`:
|
||||
|
||||
```bash
|
||||
# Install tools (macOS)
|
||||
brew install k3d kubectl mkcert dnsmasq
|
||||
|
||||
# Create k3d cluster with volume access
|
||||
k3d cluster create dev \
|
||||
--agents 2 \
|
||||
-p "80:80@loadbalancer" \
|
||||
-p "443:443@loadbalancer" \
|
||||
--volume "$HOME/projects/cannabrands_new/.worktrees:/worktrees@all"
|
||||
|
||||
# Setup wildcard DNS (*.cannabrands.test)
|
||||
echo 'address=/.cannabrands.test/127.0.0.1' | \
|
||||
sudo tee /opt/homebrew/etc/dnsmasq.d/cannabrands.conf
|
||||
sudo brew services start dnsmasq
|
||||
sudo mkdir -p /etc/resolver
|
||||
echo 'nameserver 127.0.0.1' | \
|
||||
sudo tee /etc/resolver/cannabrands.test
|
||||
|
||||
# Test DNS
|
||||
ping -c 1 anything.cannabrands.test
|
||||
# Should resolve to 127.0.0.1
|
||||
```
|
||||
|
||||
**2. Test K8s Setup in This Worktree**
|
||||
|
||||
```bash
|
||||
cd /Users/jon/projects/cannabrands/cannabrands_new/.worktrees/infra-k8s-local-dev
|
||||
|
||||
# Copy .env from another worktree
|
||||
cp ../ labs-batch-qr-codes/.env .
|
||||
|
||||
# Install dependencies (or link vendor from main)
|
||||
composer install
|
||||
|
||||
# Start k8s environment
|
||||
make k-dev
|
||||
|
||||
# Expected output:
|
||||
# - Namespace created: feat-k8s-local-dev
|
||||
# - Services deployed: PostgreSQL, Redis, Laravel app
|
||||
# - URL: http://k8s-local-dev.cannabrands.test
|
||||
|
||||
# Check status
|
||||
make k-status
|
||||
|
||||
# View logs
|
||||
make k-logs
|
||||
|
||||
# Open shell
|
||||
make k-shell
|
||||
|
||||
# Cleanup
|
||||
make k-down
|
||||
```
|
||||
|
||||
**3. Test with Multiple Worktrees**
|
||||
|
||||
```bash
|
||||
# Terminal 1: QR Code feature
|
||||
cd /Users/jon/projects/cannabrands/cannabrands_new/.worktrees/labs-batch-qr-codes
|
||||
make k-dev
|
||||
# → http://labs-batch-qr-codes.cannabrands.test
|
||||
|
||||
# Terminal 2: K8s dev feature
|
||||
cd /Users/jon/projects/cannabrands/cannabrands_new/.worktrees/infra-k8s-local-dev
|
||||
make k-dev
|
||||
# → http://k8s-local-dev.cannabrands.test
|
||||
|
||||
# Both running simultaneously with isolated namespaces!
|
||||
```
|
||||
|
||||
**4. Create Pull Request**
|
||||
|
||||
```bash
|
||||
cd /Users/jon/projects/cannabrands/cannabrands_new/.worktrees/infra-k8s-local-dev
|
||||
|
||||
# Push branch
|
||||
git push origin feature/k8s-local-dev
|
||||
|
||||
# Create PR
|
||||
gh pr create --title "feat: Add K8s local development with worktree support" \
|
||||
--body "$(cat <<EOF
|
||||
## Summary
|
||||
Adds Kubernetes local development environment that mirrors Laravel Sail
|
||||
workflow with namespace isolation per git worktree.
|
||||
|
||||
## Features
|
||||
- K8s manifests for local dev (Sail-like approach)
|
||||
- Makefile targets with \`k-\` prefix
|
||||
- Complete documentation
|
||||
- Namespace isolation per worktree
|
||||
- Wildcard domain routing (*.cannabrands.test)
|
||||
|
||||
## Setup Required
|
||||
- One-time k3d cluster setup (see docs/K8S_LOCAL_SETUP.md)
|
||||
- DNS configuration for *.cannabrands.test
|
||||
|
||||
## Testing
|
||||
- [ ] k3d cluster created
|
||||
- [ ] DNS resolves *.cannabrands.test
|
||||
- [ ] \`make k-dev\` starts successfully
|
||||
- [ ] App accessible at custom domain
|
||||
- [ ] Multiple worktrees work in parallel
|
||||
- [ ] Code changes are instant (no rebuild)
|
||||
|
||||
## Documentation
|
||||
- docs/K8S_LOCAL_SETUP.md - Complete setup guide
|
||||
- docs/K8S_LIKE_SAIL.md - Philosophy and details
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
**5. Update Team Documentation**
|
||||
|
||||
After PR is merged:
|
||||
- Add k8s setup instructions to main README
|
||||
- Announce new workflow to team
|
||||
- Consider team demo/walkthrough
|
||||
|
||||
---
|
||||
|
||||
## Workflow After Both Features Are Merged
|
||||
|
||||
### Standard Development Flow
|
||||
|
||||
```bash
|
||||
# Create new feature
|
||||
cd /Users/jon/projects/cannabrands/cannabrands_new
|
||||
git worktree add .worktrees/feature-orders feature/orders
|
||||
cd .worktrees/feature-orders
|
||||
|
||||
# Option 1: Sail (existing workflow)
|
||||
make dev
|
||||
make dev-vite
|
||||
|
||||
# Option 2: K8s (new workflow with isolation)
|
||||
make k-dev
|
||||
make k-vite
|
||||
|
||||
# Develop with instant code changes...
|
||||
|
||||
# Cleanup
|
||||
make dev-down # or make k-down
|
||||
cd ../..
|
||||
git worktree remove .worktrees/feature-orders
|
||||
```
|
||||
|
||||
### When to Use Each Mode
|
||||
|
||||
**Use Sail (`make dev`):**
|
||||
- Quick single-feature development
|
||||
- Familiar workflow
|
||||
- Lower resource usage
|
||||
|
||||
**Use K8s (`make k-dev`):**
|
||||
- Multiple features in parallel
|
||||
- Testing k8s manifests
|
||||
- Production-like environment
|
||||
- Need custom domain per feature
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### QR Code Feature Issues
|
||||
|
||||
**Redis extension error:**
|
||||
```bash
|
||||
# Solution: Use Sail instead of host PHP
|
||||
./vendor/bin/sail artisan migrate
|
||||
./vendor/bin/sail artisan test
|
||||
```
|
||||
|
||||
**Tests fail:**
|
||||
```bash
|
||||
# Ensure services are running
|
||||
docker ps | grep postgres
|
||||
docker ps | grep redis
|
||||
|
||||
# Check .env configuration
|
||||
DB_HOST=127.0.0.1 # NOT pgsql (hybrid mode)
|
||||
REDIS_HOST=127.0.0.1 # NOT redis
|
||||
```
|
||||
|
||||
### K8s Setup Issues
|
||||
|
||||
**DNS not working:**
|
||||
```bash
|
||||
# Flush DNS cache
|
||||
sudo killall -HUP mDNSResponder
|
||||
sudo brew services restart dnsmasq
|
||||
|
||||
# Test
|
||||
dig k8s-local-dev.cannabrands.test
|
||||
```
|
||||
|
||||
**Pod crashes:**
|
||||
```bash
|
||||
# Check logs
|
||||
make k-logs
|
||||
|
||||
# Common issues:
|
||||
# - Volume mount path incorrect
|
||||
# - Missing dependencies (run composer install in pod)
|
||||
# - DB connection (check PostgreSQL is ready)
|
||||
```
|
||||
|
||||
**Port conflicts:**
|
||||
```bash
|
||||
# Check what's using ports 80/443
|
||||
lsof -ti:80
|
||||
lsof -ti:443
|
||||
|
||||
# Stop conflicting services
|
||||
./vendor/bin/sail down # if Sail is running
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Questions?
|
||||
|
||||
- Check documentation: docs/K8S_LOCAL_SETUP.md and docs/K8S_LIKE_SAIL.md
|
||||
- Review git worktree workflow: docs/GIT_WORKTREE_WORKFLOW.md
|
||||
- Ask in team chat
|
||||
|
||||
---
|
||||
|
||||
**Generated:** 2025-10-31
|
||||
**Author:** Claude Code with Human Partner
|
||||
539
docs/K8S_LIKE_SAIL.md
Normal file
539
docs/K8S_LIKE_SAIL.md
Normal file
@@ -0,0 +1,539 @@
|
||||
# K8s Local Development (Sail-Style - No Image Builds)
|
||||
|
||||
**Goal:** Use k8s for local dev exactly like Laravel Sail - no image building, instant code changes.
|
||||
|
||||
---
|
||||
|
||||
## Philosophy: Development Should Be Fast
|
||||
|
||||
### How Sail Works (and Why We Love It)
|
||||
```bash
|
||||
./vendor/bin/sail up
|
||||
# ✅ Uses pre-built PHP image
|
||||
# ✅ Mounts your code as volume
|
||||
# ✅ Code changes are instant
|
||||
# ✅ No docker build needed
|
||||
# ✅ Composer/artisan run inside container
|
||||
```
|
||||
|
||||
### How K8s Should Work (Same Experience)
|
||||
```bash
|
||||
make k8s-up
|
||||
# ✅ Uses pre-built PHP image
|
||||
# ✅ Mounts your code from worktree
|
||||
# ✅ Code changes are instant
|
||||
# ✅ No docker build needed
|
||||
# ✅ Composer/artisan run inside pod
|
||||
```
|
||||
|
||||
**The only difference:** k8s gives you namespace isolation + nice URLs.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Sail Setup
|
||||
```yaml
|
||||
# docker-compose.yml (simplified)
|
||||
services:
|
||||
laravel.test:
|
||||
image: sail-8.2/app # Pre-built
|
||||
volumes:
|
||||
- './:/var/www/html' # Code mounted
|
||||
depends_on:
|
||||
- pgsql
|
||||
- redis
|
||||
```
|
||||
|
||||
### K8s Setup (Same Concept)
|
||||
```yaml
|
||||
# k8s deployment (simplified)
|
||||
spec:
|
||||
containers:
|
||||
- name: web
|
||||
image: thecodingmachine/php:8.2-apache # Pre-built
|
||||
volumeMounts:
|
||||
- name: code
|
||||
mountPath: /var/www/html # Code mounted from worktree
|
||||
volumes:
|
||||
- name: code
|
||||
hostPath:
|
||||
path: /worktrees/feature-name # Your worktree
|
||||
```
|
||||
|
||||
**Both mount code from disk → instant changes!**
|
||||
|
||||
---
|
||||
|
||||
## Key Insight: Base Images vs Custom Images
|
||||
|
||||
### Base Image (For Development) ✅
|
||||
```dockerfile
|
||||
# Use existing image (like Sail)
|
||||
thecodingmachine/php:8.2-apache
|
||||
# or
|
||||
laravelsail/php82-composer:latest
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- PHP 8.2
|
||||
- Apache/Nginx
|
||||
- Composer
|
||||
- Common extensions (pgsql, redis, gd, etc.)
|
||||
- Node.js (for Vite)
|
||||
|
||||
**Mount your code** → no build needed!
|
||||
|
||||
### Custom Image (For Production) 🚢
|
||||
```dockerfile
|
||||
# Build your own (for deployment)
|
||||
FROM php:8.2-fpm-alpine
|
||||
COPY . /var/www/html
|
||||
RUN composer install --no-dev
|
||||
# etc...
|
||||
```
|
||||
|
||||
**Only build this for:**
|
||||
- CI/CD testing
|
||||
- Production deployment
|
||||
- Final PR verification
|
||||
|
||||
---
|
||||
|
||||
## Complete K8s Setup (Sail-Style)
|
||||
|
||||
### 1. Create k3d Cluster with Volume Access
|
||||
|
||||
```bash
|
||||
# One-time setup
|
||||
k3d cluster create dev \
|
||||
--agents 2 \
|
||||
-p "80:80@loadbalancer" \
|
||||
-p "443:443@loadbalancer" \
|
||||
--volume "$HOME/projects/cannabrands_new/.worktrees:/worktrees@all"
|
||||
```
|
||||
|
||||
This mounts your `.worktrees/` directory into all k3d nodes.
|
||||
|
||||
### 2. K8s Manifests (No Image Build)
|
||||
|
||||
**k8s/local/deployment.yaml**
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: web
|
||||
namespace: ${NS}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: web
|
||||
spec:
|
||||
# Run composer install on startup (like Sail)
|
||||
initContainers:
|
||||
- name: composer-install
|
||||
image: composer:2
|
||||
workingDir: /var/www/html
|
||||
command: ['composer', 'install', '--no-interaction']
|
||||
volumeMounts:
|
||||
- name: code
|
||||
mountPath: /var/www/html
|
||||
|
||||
containers:
|
||||
- name: web
|
||||
image: thecodingmachine/php:8.2-apache
|
||||
ports:
|
||||
- containerPort: 80
|
||||
workingDir: /var/www/html
|
||||
|
||||
# Environment variables
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: app-env
|
||||
|
||||
# Your code mounted from worktree
|
||||
volumeMounts:
|
||||
- name: code
|
||||
mountPath: /var/www/html
|
||||
|
||||
# Startup command (like Sail)
|
||||
command: ["/bin/bash", "-c"]
|
||||
args:
|
||||
- |
|
||||
# Wait for DB
|
||||
until pg_isready -h postgres -U ${DB_USERNAME}; do
|
||||
echo "Waiting for PostgreSQL..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Run migrations (like Sail auto-migration)
|
||||
php artisan migrate --force
|
||||
|
||||
# Start Apache
|
||||
apache2-foreground
|
||||
|
||||
# Health check
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
|
||||
# Mount code from k3d volume (which mounts your worktree)
|
||||
volumes:
|
||||
- name: code
|
||||
hostPath:
|
||||
path: /worktrees/${WORKTREE_NAME}
|
||||
type: Directory
|
||||
```
|
||||
|
||||
**k8s/local/postgres.yaml** (same as before)
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: ${NS}
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: ${NS}
|
||||
spec:
|
||||
serviceName: postgres
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: pg-auth
|
||||
volumeMounts:
|
||||
- name: pgdata
|
||||
mountPath: /var/lib/postgresql/data
|
||||
subPath: data
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: pgdata
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
```
|
||||
|
||||
**k8s/local/redis.yaml**
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: ${NS}
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- port: 6379
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: ${NS}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
```
|
||||
|
||||
### 3. Makefile (One Command Like Sail)
|
||||
|
||||
```makefile
|
||||
k8s-up: ## Start k8s environment (like 'sail up')
|
||||
@echo "🚀 Starting k8s environment for: $(WORKTREE_NAME)"
|
||||
@# Create namespace
|
||||
@kubectl create ns $(K8S_NS) --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
@# Create secrets from .env
|
||||
@kubectl -n $(K8S_NS) delete secret app-env --ignore-not-found
|
||||
@kubectl -n $(K8S_NS) create secret generic app-env --from-env-file=.env
|
||||
|
||||
@# Create PostgreSQL
|
||||
@export NS=$(K8S_NS) PG_DB=app_$(SANITIZED_BRANCH) PG_USER=app PG_PASS=secret && \
|
||||
envsubst < k8s/local/postgres.yaml | kubectl apply -f -
|
||||
|
||||
@# Create Redis
|
||||
@export NS=$(K8S_NS) && \
|
||||
envsubst < k8s/local/redis.yaml | kubectl apply -f -
|
||||
|
||||
@# Wait for DB
|
||||
@kubectl -n $(K8S_NS) rollout status sts/postgres --timeout=60s
|
||||
|
||||
@# Deploy app (with code volume mounted)
|
||||
@export NS=$(K8S_NS) WORKTREE_NAME=$(WORKTREE_NAME) && \
|
||||
envsubst < k8s/local/deployment.yaml | kubectl apply -f -
|
||||
|
||||
@# Create service + ingress
|
||||
@export NS=$(K8S_NS) HOST=$(K8S_HOST) && \
|
||||
envsubst < k8s/local/service.yaml | kubectl apply -f - && \
|
||||
envsubst < k8s/local/ingress.yaml | kubectl apply -f -
|
||||
|
||||
@echo ""
|
||||
@echo "✅ Ready! Visit: http://$(K8S_HOST)"
|
||||
@echo ""
|
||||
@echo "💡 Your code is volume-mounted - changes are instant!"
|
||||
@echo " Edit files → refresh browser → see changes"
|
||||
|
||||
k8s-down: ## Stop k8s environment (like 'sail down')
|
||||
@echo "🗑 Removing namespace: $(K8S_NS)"
|
||||
@kubectl delete ns $(K8S_NS) --ignore-not-found
|
||||
@echo "✅ Cleaned up"
|
||||
|
||||
k8s-logs: ## View app logs (like 'sail logs')
|
||||
@kubectl -n $(K8S_NS) logs -f deploy/web
|
||||
|
||||
k8s-shell: ## Shell into app container (like 'sail shell')
|
||||
@kubectl -n $(K8S_NS) exec -it deploy/web -- /bin/bash
|
||||
|
||||
k8s-artisan: ## Run artisan command (like 'sail artisan')
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- php artisan $(CMD)
|
||||
|
||||
k8s-composer: ## Run composer (like 'sail composer')
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- composer $(CMD)
|
||||
|
||||
k8s-npm: ## Run npm (like 'sail npm')
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- npm $(CMD)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Comparison
|
||||
|
||||
### Sail Commands
|
||||
```bash
|
||||
./vendor/bin/sail up -d
|
||||
./vendor/bin/sail artisan migrate
|
||||
./vendor/bin/sail composer install
|
||||
./vendor/bin/sail npm run dev
|
||||
./vendor/bin/sail shell
|
||||
./vendor/bin/sail down
|
||||
```
|
||||
|
||||
### K8s Commands (Same Experience)
|
||||
```bash
|
||||
make k8s-up
|
||||
make k8s-artisan CMD=migrate
|
||||
make k8s-composer CMD=install
|
||||
make k8s-npm CMD="run dev"
|
||||
make k8s-shell
|
||||
make k8s-down
|
||||
```
|
||||
|
||||
**Nearly identical workflow!**
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Start Feature
|
||||
```bash
|
||||
git worktree add .worktrees/feature-orders feature/orders
|
||||
cd .worktrees/feature-orders
|
||||
```
|
||||
|
||||
### 2. Start K8s Environment (Like Sail)
|
||||
```bash
|
||||
make k8s-up
|
||||
# → Creates namespace
|
||||
# → Starts PostgreSQL, Redis
|
||||
# → Mounts YOUR CODE from worktree
|
||||
# → Runs migrations
|
||||
# → Available at: http://orders.cannabrands.test
|
||||
```
|
||||
|
||||
### 3. Develop (Instant Changes)
|
||||
```bash
|
||||
# Edit code in your IDE
|
||||
vim app/Http/Controllers/OrderController.php
|
||||
|
||||
# Changes are INSTANT (volume mounted)
|
||||
# Refresh browser → see changes ✅
|
||||
|
||||
# Run commands in pod
|
||||
make k8s-artisan CMD="make:model Product"
|
||||
make k8s-composer CMD="require package/name"
|
||||
make k8s-npm CMD="install"
|
||||
```
|
||||
|
||||
### 4. Multiple Features (Parallel)
|
||||
```bash
|
||||
# Terminal 1: Feature A
|
||||
cd .worktrees/feature-orders
|
||||
make k8s-up
|
||||
# → http://orders.cannabrands.test
|
||||
|
||||
# Terminal 2: Feature B
|
||||
cd .worktrees/feature-payments
|
||||
make k8s-up
|
||||
# → http://payments.cannabrands.test
|
||||
|
||||
# Both running, completely isolated! ✅
|
||||
```
|
||||
|
||||
### 5. Cleanup
|
||||
```bash
|
||||
make k8s-down
|
||||
# → Deletes namespace
|
||||
# → Removes PostgreSQL, Redis
|
||||
# → That's it! ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Benefits Over Sail
|
||||
|
||||
| Feature | Sail | K8s |
|
||||
|---------|------|-----|
|
||||
| **Code volume mounted** | ✅ | ✅ |
|
||||
| **No image builds** | ✅ | ✅ |
|
||||
| **Instant code changes** | ✅ | ✅ |
|
||||
| **Isolated environments** | ❌ (shared services) | ✅ (namespaces) |
|
||||
| **Custom domains** | ❌ (localhost ports) | ✅ (*.cannabrands.test) |
|
||||
| **Test k8s configs** | ❌ | ✅ |
|
||||
| **Production parity** | ❌ | ✅ |
|
||||
| **Multiple features parallel** | ⚠️ (port conflicts) | ✅ (easy) |
|
||||
|
||||
---
|
||||
|
||||
## When You DO Build Images
|
||||
|
||||
**Only build images for:**
|
||||
|
||||
1. **CI/CD Testing**
|
||||
```bash
|
||||
# In GitHub Actions
|
||||
docker build -t cannabrands:$BRANCH .
|
||||
# Test the build works
|
||||
```
|
||||
|
||||
2. **Production Deployment**
|
||||
```bash
|
||||
# For production k8s cluster
|
||||
docker build -t registry.example.com/cannabrands:v1.2.3 .
|
||||
docker push registry.example.com/cannabrands:v1.2.3
|
||||
```
|
||||
|
||||
3. **Final PR Verification** (optional)
|
||||
```bash
|
||||
# Test production Dockerfile locally
|
||||
make k8s-test-prod-build
|
||||
```
|
||||
|
||||
**But for daily feature development:** NO IMAGE BUILDS! ✅
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Code Changes Not Showing
|
||||
|
||||
```bash
|
||||
# 1. Check volume mount
|
||||
kubectl -n $(K8S_NS) exec deploy/web -- ls -la /var/www/html
|
||||
|
||||
# Should show your worktree files
|
||||
|
||||
# 2. Clear Laravel caches
|
||||
make k8s-artisan CMD="cache:clear"
|
||||
make k8s-artisan CMD="config:clear"
|
||||
make k8s-artisan CMD="view:clear"
|
||||
|
||||
# 3. Check file permissions (rare)
|
||||
kubectl -n $(K8S_NS) exec deploy/web -- chown -R www-data:www-data /var/www/html/storage
|
||||
```
|
||||
|
||||
### Composer Install Fails
|
||||
|
||||
```bash
|
||||
# Run composer inside pod (has all PHP extensions)
|
||||
make k8s-composer CMD="install"
|
||||
|
||||
# If needed, clear cache
|
||||
make k8s-composer CMD="clear-cache"
|
||||
```
|
||||
|
||||
### Database Connection Error
|
||||
|
||||
```bash
|
||||
# Check PostgreSQL is ready
|
||||
kubectl -n $(K8S_NS) get pods
|
||||
|
||||
# If not ready, wait
|
||||
kubectl -n $(K8S_NS) rollout status sts/postgres
|
||||
|
||||
# Check connection from app
|
||||
make k8s-shell
|
||||
# Inside pod:
|
||||
php artisan tinker
|
||||
>>> DB::connection()->getPdo();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**K8s local dev can work EXACTLY like Sail:**
|
||||
|
||||
1. ✅ Use pre-built PHP image (no custom builds)
|
||||
2. ✅ Mount code as volumes (instant changes)
|
||||
3. ✅ Run composer/artisan inside pods
|
||||
4. ✅ One command to start/stop (`make k8s-up/down`)
|
||||
|
||||
**Bonus benefits:**
|
||||
- 🎯 Namespace isolation (multiple features parallel)
|
||||
- 🌐 Nice URLs (*.cannabrands.test)
|
||||
- 🚀 Tests k8s configs locally
|
||||
- 🏢 Production parity
|
||||
|
||||
**The workflow IS sail, just orchestrated by k8s instead of docker-compose.**
|
||||
|
||||
---
|
||||
|
||||
**Next Steps:**
|
||||
1. Run one-time k3d cluster setup
|
||||
2. Test with one worktree: `make k8s-up`
|
||||
3. Edit code → see instant changes
|
||||
4. Use for all feature development ✅
|
||||
370
docs/K8S_LOCAL_SETUP.md
Normal file
370
docs/K8S_LOCAL_SETUP.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# Local Kubernetes Development with k3d + Git Worktrees
|
||||
|
||||
**Goal:** Each git worktree gets its own isolated k8s namespace at `[branch-name].cannabrands.test`
|
||||
|
||||
---
|
||||
|
||||
## One-Time Setup (Do This Once)
|
||||
|
||||
### 1. Install Required Tools
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install k3d kubectl mkcert dnsmasq
|
||||
|
||||
# Verify installation
|
||||
k3d version
|
||||
kubectl version --client
|
||||
```
|
||||
|
||||
### 2. Create k3d Cluster
|
||||
|
||||
```bash
|
||||
# Create cluster with Traefik ingress
|
||||
k3d cluster create dev \
|
||||
--agents 2 \
|
||||
-p "80:80@loadbalancer" \
|
||||
-p "443:443@loadbalancer"
|
||||
|
||||
# Verify cluster
|
||||
kubectl cluster-info
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
- Creates 3-node cluster (1 server, 2 agents)
|
||||
- Exposes ports 80/443 for web traffic
|
||||
- Includes Traefik ingress controller (built-in)
|
||||
|
||||
### 3. Setup Wildcard DNS (*.cannabrands.test → 127.0.0.1)
|
||||
|
||||
```bash
|
||||
# Configure dnsmasq to resolve *.cannabrands.test
|
||||
echo 'address=/.cannabrands.test/127.0.0.1' | \
|
||||
sudo tee /opt/homebrew/etc/dnsmasq.d/cannabrands.conf
|
||||
|
||||
# Start dnsmasq
|
||||
sudo brew services start dnsmasq
|
||||
|
||||
# Tell macOS to use dnsmasq for .test domains
|
||||
sudo mkdir -p /etc/resolver
|
||||
echo 'nameserver 127.0.0.1' | \
|
||||
sudo tee /etc/resolver/cannabrands.test
|
||||
|
||||
# Test it works
|
||||
ping -c 1 anything.cannabrands.test
|
||||
# Should resolve to 127.0.0.1
|
||||
```
|
||||
|
||||
### 4. (Optional) Setup HTTPS with mkcert
|
||||
|
||||
```bash
|
||||
# Install local CA
|
||||
mkcert -install
|
||||
|
||||
# Generate wildcard certificate
|
||||
mkcert "*.cannabrands.test"
|
||||
|
||||
# Create k8s secret (in kube-system for Traefik)
|
||||
kubectl -n kube-system create secret tls cannabrands-test-tls \
|
||||
--cert="_wildcard.cannabrands.test.pem" \
|
||||
--key="_wildcard.cannabrands.test-key.pem"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Per-Worktree Usage
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# From a worktree directory
|
||||
cd .worktrees/labs-batch-qr-codes
|
||||
|
||||
# Spin up k8s environment
|
||||
make k8s-up
|
||||
|
||||
# Access your app
|
||||
open http://labs-batch-qr-codes.cannabrands.test
|
||||
|
||||
# Tear down when done
|
||||
make k8s-down
|
||||
```
|
||||
|
||||
### What `make k8s-up` Does
|
||||
|
||||
1. **Detects branch name** → namespace name
|
||||
- Branch: `feature/batch-qr-codes`
|
||||
- Namespace: `feat-batch-qr-codes`
|
||||
- URL: `batch-qr-codes.cannabrands.test`
|
||||
|
||||
2. **Builds Docker image** from current worktree
|
||||
- Tag: `cannabrands:batch-qr-codes`
|
||||
- Includes all your code changes
|
||||
|
||||
3. **Imports image** into k3d cluster
|
||||
|
||||
4. **Creates namespace** with isolated resources:
|
||||
- PostgreSQL database (with PVC)
|
||||
- Redis cache
|
||||
- Laravel app deployment
|
||||
- Services + Ingress
|
||||
|
||||
5. **Runs migrations** automatically
|
||||
|
||||
6. **Shows URL** when ready
|
||||
|
||||
### What `make k8s-down` Does
|
||||
|
||||
1. **Deletes namespace** (removes everything)
|
||||
2. **Removes Docker image** (cleanup)
|
||||
3. **That's it!** No orphaned resources
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Namespace Isolation
|
||||
|
||||
```
|
||||
k3d cluster "dev"
|
||||
├── namespace: feat-batch-qr-codes
|
||||
│ ├── PostgreSQL (1Gi PVC)
|
||||
│ ├── Redis
|
||||
│ ├── Laravel app (your code)
|
||||
│ └── Ingress → batch-qr-codes.cannabrands.test
|
||||
│
|
||||
├── namespace: feat-order-flow
|
||||
│ ├── PostgreSQL (1Gi PVC)
|
||||
│ ├── Redis
|
||||
│ ├── Laravel app (different code)
|
||||
│ └── Ingress → order-flow.cannabrands.test
|
||||
│
|
||||
└── namespace: kube-system
|
||||
└── Traefik (shared ingress controller)
|
||||
```
|
||||
|
||||
**Each namespace is completely isolated:**
|
||||
- ✅ Own database
|
||||
- ✅ Own Redis
|
||||
- ✅ Own environment variables
|
||||
- ✅ Own code version
|
||||
- ✅ Own URL
|
||||
|
||||
### Why This Works
|
||||
|
||||
1. **True Production Parity**
|
||||
- Uses same k8s manifests as production
|
||||
- Tests deployments, services, ingress
|
||||
- Catches k8s-specific issues locally
|
||||
|
||||
2. **Complete Isolation**
|
||||
- No shared state between features
|
||||
- Can't accidentally break other features
|
||||
- Clean teardown = delete namespace
|
||||
|
||||
3. **Git Worktree Perfect Match**
|
||||
- One worktree = one namespace
|
||||
- Code changes in worktree → image rebuild
|
||||
- Delete worktree → delete namespace
|
||||
|
||||
4. **Better Than Hybrid for:**
|
||||
- Testing k8s configs
|
||||
- Multi-service features
|
||||
- Production debugging
|
||||
|
||||
---
|
||||
|
||||
## Comparison: Hybrid vs K8s
|
||||
|
||||
### Hybrid Mode (Native Laravel + Docker Compose)
|
||||
|
||||
```bash
|
||||
cd .worktrees/feature-a
|
||||
php artisan serve --port=8000 # http://localhost:8000
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- ✅ Faster startup (~5 seconds)
|
||||
- ✅ Simpler (no k8s knowledge needed)
|
||||
- ✅ Lower resource usage
|
||||
|
||||
**Cons:**
|
||||
- ❌ Doesn't test k8s configs
|
||||
- ❌ Port-based URLs (8000, 8001)
|
||||
- ❌ Not production-like
|
||||
|
||||
### K8s Mode (k3d)
|
||||
|
||||
```bash
|
||||
cd .worktrees/feature-a
|
||||
make k8s-up # http://feature-a.cannabrands.test
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- ✅ Production parity
|
||||
- ✅ Tests k8s manifests
|
||||
- ✅ Nice URLs (*.cannabrands.test)
|
||||
- ✅ Complete isolation
|
||||
|
||||
**Cons:**
|
||||
- ❌ Slower startup (~30 seconds)
|
||||
- ❌ More resources (Docker overhead)
|
||||
- ❌ Requires k8s knowledge
|
||||
|
||||
### Recommendation
|
||||
|
||||
**Use both!**
|
||||
|
||||
```bash
|
||||
# 90% of time: Quick iterations
|
||||
make serve # Hybrid mode
|
||||
|
||||
# 10% of time: Pre-merge testing
|
||||
make k8s-up # K8s mode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### DNS Not Working
|
||||
|
||||
```bash
|
||||
# Test resolution
|
||||
dig anything.cannabrands.test
|
||||
|
||||
# Should show:
|
||||
# ;; ANSWER SECTION:
|
||||
# anything.cannabrands.test. 0 IN A 127.0.0.1
|
||||
|
||||
# If not:
|
||||
sudo killall -HUP mDNSResponder # Flush DNS cache
|
||||
sudo brew services restart dnsmasq
|
||||
```
|
||||
|
||||
### k3d Cluster Issues
|
||||
|
||||
```bash
|
||||
# Check cluster status
|
||||
k3d cluster list
|
||||
kubectl get nodes
|
||||
|
||||
# Restart cluster
|
||||
k3d cluster stop dev
|
||||
k3d cluster start dev
|
||||
|
||||
# Nuclear option (delete and recreate)
|
||||
k3d cluster delete dev
|
||||
k3d cluster create dev --agents 2 -p "80:80@loadbalancer" -p "443:443@loadbalancer"
|
||||
```
|
||||
|
||||
### Image Not Found in Cluster
|
||||
|
||||
```bash
|
||||
# Verify image imported
|
||||
k3d image list -c dev | grep cannabrands
|
||||
|
||||
# Manual import
|
||||
k3d image import cannabrands:branch-name -c dev
|
||||
```
|
||||
|
||||
### Namespace Stuck Deleting
|
||||
|
||||
```bash
|
||||
# Force delete
|
||||
kubectl delete ns feat-branch-name --grace-period=0 --force
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
```bash
|
||||
# Find what's using port 80
|
||||
lsof -ti:80
|
||||
|
||||
# Stop Traefik if needed
|
||||
kubectl -n kube-system scale deploy/traefik --replicas=0
|
||||
|
||||
# Restart
|
||||
kubectl -n kube-system scale deploy/traefik --replicas=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Share Environment with Teammate
|
||||
|
||||
```bash
|
||||
# Export namespace manifests
|
||||
kubectl get all -n feat-branch-name -o yaml > my-feature.yaml
|
||||
|
||||
# Teammate applies
|
||||
kubectl apply -f my-feature.yaml
|
||||
```
|
||||
|
||||
### Access Database Directly
|
||||
|
||||
```bash
|
||||
# Port-forward PostgreSQL
|
||||
kubectl -n feat-batch-qr-codes port-forward svc/postgres 5433:5432
|
||||
|
||||
# Connect with client
|
||||
psql -h localhost -p 5433 -U app -d app_batch_qr_codes
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# App logs
|
||||
kubectl -n feat-batch-qr-codes logs -f deploy/web
|
||||
|
||||
# Postgres logs
|
||||
kubectl -n feat-batch-qr-codes logs -f sts/postgres
|
||||
|
||||
# All logs
|
||||
kubectl -n feat-batch-qr-codes logs -f --all-containers=true -l app=web
|
||||
```
|
||||
|
||||
### Shell into Pod
|
||||
|
||||
```bash
|
||||
# Laravel app
|
||||
kubectl -n feat-batch-qr-codes exec -it deploy/web -- /bin/bash
|
||||
|
||||
# Run artisan commands
|
||||
kubectl -n feat-batch-qr-codes exec deploy/web -- php artisan migrate
|
||||
kubectl -n feat-batch-qr-codes exec deploy/web -- php artisan tinker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resource Usage
|
||||
|
||||
### Typical k3d Overhead
|
||||
|
||||
```
|
||||
k3d cluster: ~500MB RAM
|
||||
Per namespace: ~300-500MB RAM
|
||||
- PostgreSQL: ~100MB
|
||||
- Redis: ~50MB
|
||||
- Laravel app: ~150-300MB
|
||||
```
|
||||
|
||||
**Total for 3 features running:** ~2GB RAM
|
||||
|
||||
Compare to Hybrid Mode: ~200MB per worktree
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Complete one-time setup above
|
||||
2. ✅ Test with: `make k8s-up` in a worktree
|
||||
3. ✅ Verify URL works: `http://[branch].cannabrands.test`
|
||||
4. ✅ Tear down: `make k8s-down`
|
||||
5. ✅ Add to your daily workflow
|
||||
|
||||
---
|
||||
|
||||
**Questions?** See `docs/GIT_WORKTREE_WORKFLOW.md` or ask in team chat.
|
||||
130
k8s/local/deployment.yaml
Normal file
130
k8s/local/deployment.yaml
Normal file
@@ -0,0 +1,130 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: web
|
||||
namespace: ${NS}
|
||||
labels:
|
||||
app: web
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: web
|
||||
spec:
|
||||
# Sail-like approach: composer install runs inside container at startup
|
||||
# No pre-installation needed on host - everything happens in the pod
|
||||
|
||||
containers:
|
||||
# Main Laravel application container
|
||||
- name: web
|
||||
image: thecodingmachine/php:8.3-v5-apache-node20
|
||||
# Note: PHP 8.3 matches production, intl extension available via PHP_EXTENSIONS
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
- containerPort: 5173
|
||||
name: vite
|
||||
workingDir: /var/www/html
|
||||
|
||||
# Environment variables from .env
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: app-env
|
||||
|
||||
# Additional required environment variables for k8s
|
||||
env:
|
||||
- name: DB_HOST
|
||||
value: postgres
|
||||
- name: REDIS_HOST
|
||||
value: redis
|
||||
- name: APP_URL
|
||||
value: http://${K8S_HOST}
|
||||
- name: VITE_APP_URL
|
||||
value: http://${K8S_HOST}
|
||||
- name: PHP_EXTENSIONS
|
||||
value: "intl pdo_pgsql pgsql redis gd zip bcmath"
|
||||
- name: ABSOLUTE_APACHE_DOCUMENT_ROOT
|
||||
value: "/var/www/html/public"
|
||||
|
||||
# Mount code from worktree via k3d volume
|
||||
volumeMounts:
|
||||
- name: code
|
||||
mountPath: /var/www/html
|
||||
|
||||
# Startup command
|
||||
command: ["/bin/bash", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -e
|
||||
|
||||
echo "Waiting for PostgreSQL..."
|
||||
until timeout 1 bash -c 'cat < /dev/null > /dev/tcp/postgres/5432' 2>/dev/null; do
|
||||
echo "PostgreSQL is unavailable - sleeping"
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL is ready!"
|
||||
|
||||
echo "Waiting for Redis..."
|
||||
until timeout 1 bash -c 'cat < /dev/null > /dev/tcp/redis/6379' 2>/dev/null; do
|
||||
echo "Redis is unavailable - sleeping"
|
||||
sleep 2
|
||||
done
|
||||
echo "Redis is ready!"
|
||||
|
||||
echo "Installing/updating composer dependencies..."
|
||||
composer install --no-interaction --prefer-dist
|
||||
|
||||
echo "Installing/updating npm dependencies..."
|
||||
npm install --no-audit --no-fund
|
||||
|
||||
echo "Building frontend assets..."
|
||||
npm run build
|
||||
|
||||
echo "Running migrations..."
|
||||
php artisan migrate --force
|
||||
|
||||
echo "Clearing caches..."
|
||||
php artisan cache:clear
|
||||
php artisan config:clear
|
||||
php artisan route:clear
|
||||
php artisan view:clear
|
||||
|
||||
echo "Starting Apache..."
|
||||
apache2-foreground
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 90
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 6
|
||||
|
||||
# Mount code from k3d volume (which mounts your worktree)
|
||||
volumes:
|
||||
- name: code
|
||||
hostPath:
|
||||
path: /worktrees/${WORKTREE_NAME}
|
||||
type: Directory
|
||||
33
k8s/local/ingress.yaml
Normal file
33
k8s/local/ingress.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: web
|
||||
namespace: ${NS}
|
||||
annotations:
|
||||
# Traefik annotations
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||
spec:
|
||||
rules:
|
||||
# Main application domain
|
||||
- host: ${K8S_HOST}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: web
|
||||
port:
|
||||
number: 80
|
||||
|
||||
# Vite HMR domain (for hot module replacement)
|
||||
- host: vite.${K8S_HOST}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: web
|
||||
port:
|
||||
number: 5173
|
||||
7
k8s/local/namespace.yaml
Normal file
7
k8s/local/namespace.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: ${NS}
|
||||
labels:
|
||||
environment: local
|
||||
worktree: ${WORKTREE_NAME}
|
||||
84
k8s/local/postgres.yaml
Normal file
84
k8s/local/postgres.yaml
Normal file
@@ -0,0 +1,84 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: pg-auth
|
||||
namespace: ${NS}
|
||||
type: Opaque
|
||||
stringData:
|
||||
POSTGRES_DB: ${PG_DB}
|
||||
POSTGRES_USER: ${PG_USER}
|
||||
POSTGRES_PASSWORD: ${PG_PASS}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: ${NS}
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
clusterIP: None # Headless service for StatefulSet
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: ${NS}
|
||||
spec:
|
||||
serviceName: postgres
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
name: postgres
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: pg-auth
|
||||
volumeMounts:
|
||||
- name: pgdata
|
||||
mountPath: /var/lib/postgresql/data
|
||||
subPath: data # Prevent PostgreSQL from failing on non-empty directory
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- ${PG_USER}
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- ${PG_USER}
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: pgdata
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
54
k8s/local/redis.yaml
Normal file
54
k8s/local/redis.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: ${NS}
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: ${NS}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
name: redis
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "200m"
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- redis-cli
|
||||
- ping
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- redis-cli
|
||||
- ping
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
20
k8s/local/service.yaml
Normal file
20
k8s/local/service.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: web
|
||||
namespace: ${NS}
|
||||
labels:
|
||||
app: web
|
||||
spec:
|
||||
selector:
|
||||
app: web
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
protocol: TCP
|
||||
- name: vite
|
||||
port: 5173
|
||||
targetPort: 5173
|
||||
protocol: TCP
|
||||
type: ClusterIP
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "cannabrands_new",
|
||||
"name": "html",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
105
scripts/setup-local-dns.sh
Executable file
105
scripts/setup-local-dns.sh
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Local DNS Setup for K8s Development
|
||||
#
|
||||
# This script configures *.cannabrands.test to resolve to 127.0.0.1
|
||||
# Run once per developer machine. Safe to run multiple times (idempotent).
|
||||
#
|
||||
# Requirements: dnsmasq installed via Homebrew
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
echo ""
|
||||
echo "🌐 Local DNS Setup for *.cannabrands.test"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Check if running on macOS
|
||||
if [[ "$(uname)" != "Darwin" ]]; then
|
||||
echo "❌ Error: This script is for macOS only"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if dnsmasq is installed
|
||||
if ! command -v dnsmasq &> /dev/null; then
|
||||
echo "❌ Error: dnsmasq not found"
|
||||
echo " Install with: brew install dnsmasq"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. Configure dnsmasq
|
||||
echo "📝 Step 1/4: Configuring dnsmasq..."
|
||||
DNSMASQ_CONF="/opt/homebrew/etc/dnsmasq.d/cannabrands.conf"
|
||||
|
||||
if [[ -f "$DNSMASQ_CONF" ]]; then
|
||||
echo " ℹ️ Config already exists: $DNSMASQ_CONF"
|
||||
echo " Current content: $(cat $DNSMASQ_CONF)"
|
||||
else
|
||||
sudo mkdir -p /opt/homebrew/etc/dnsmasq.d
|
||||
echo 'address=/.cannabrands.test/127.0.0.1' | sudo tee "$DNSMASQ_CONF" > /dev/null
|
||||
echo " ✅ Created: $DNSMASQ_CONF"
|
||||
fi
|
||||
|
||||
# 2. Restart dnsmasq
|
||||
echo ""
|
||||
echo "🔄 Step 2/4: Restarting dnsmasq service..."
|
||||
sudo brew services restart dnsmasq
|
||||
sleep 2 # Give dnsmasq time to start
|
||||
|
||||
# Check if dnsmasq is running
|
||||
if brew services list | grep -q "dnsmasq.*started"; then
|
||||
echo " ✅ dnsmasq is running"
|
||||
else
|
||||
echo " ⚠️ Warning: dnsmasq may not be running properly"
|
||||
echo " Check with: brew services list"
|
||||
fi
|
||||
|
||||
# 3. Configure macOS resolver
|
||||
echo ""
|
||||
echo "📝 Step 3/4: Configuring macOS resolver..."
|
||||
RESOLVER_CONF="/etc/resolver/cannabrands.test"
|
||||
|
||||
if [[ -f "$RESOLVER_CONF" ]]; then
|
||||
echo " ℹ️ Resolver already exists: $RESOLVER_CONF"
|
||||
echo " Current content: $(cat $RESOLVER_CONF)"
|
||||
else
|
||||
sudo mkdir -p /etc/resolver
|
||||
echo 'nameserver 127.0.0.1' | sudo tee "$RESOLVER_CONF" > /dev/null
|
||||
echo " ✅ Created: $RESOLVER_CONF"
|
||||
fi
|
||||
|
||||
# 4. Flush DNS cache
|
||||
echo ""
|
||||
echo "🧹 Step 4/4: Flushing macOS DNS cache..."
|
||||
sudo killall -HUP mDNSResponder 2>/dev/null || true
|
||||
echo " ✅ DNS cache flushed"
|
||||
|
||||
# Test DNS resolution
|
||||
echo ""
|
||||
echo "🧪 Testing DNS resolution..."
|
||||
sleep 1 # Give resolver time to pick up changes
|
||||
|
||||
if ping -c 1 -W 2 test.cannabrands.test &>/dev/null; then
|
||||
echo " ✅ SUCCESS! test.cannabrands.test resolves to 127.0.0.1"
|
||||
echo ""
|
||||
echo "✅ DNS Setup Complete!"
|
||||
echo ""
|
||||
echo "All *.cannabrands.test domains now resolve to 127.0.0.1"
|
||||
echo "You can now use: make k-dev in any worktree"
|
||||
else
|
||||
echo " ⚠️ Warning: DNS test failed"
|
||||
echo ""
|
||||
echo "Troubleshooting:"
|
||||
echo "1. Check dnsmasq: brew services list"
|
||||
echo "2. Test manually: dig test.cannabrands.test"
|
||||
echo "3. Check config: cat $DNSMASQ_CONF"
|
||||
echo "4. Check resolver: cat $RESOLVER_CONF"
|
||||
echo ""
|
||||
echo "If issues persist, try:"
|
||||
echo " - Restart your terminal"
|
||||
echo " - Restart your Mac"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
Reference in New Issue
Block a user