Add Gitea, Cloudflare DNS, and Kubernetes deployer adapters following hexagonal architecture. These enable automated project provisioning: - Git repository creation/management via Gitea - DNS record management via Cloudflare - Container deployment to Kubernetes Includes domain models, ports, handlers, and Woodpecker CI webhook integration for automated deployments on push. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1121 lines
32 KiB
Markdown
1121 lines
32 KiB
Markdown
# threesix.ai Infrastructure Implementation Plan
|
|
|
|
> Self-hosted git, CI/CD, and deployment infrastructure for agent-driven development.
|
|
|
|
## Overview
|
|
|
|
Replace GitHub dependency with self-hosted infrastructure on k3s:
|
|
- **Gitea** - Git server (full-featured, web UI, native Woodpecker integration)
|
|
- **Zot** - Container registry (OCI-native)
|
|
- **Woodpecker** - CI/CD pipelines
|
|
- **rdev-api** - Orchestration layer with DNS management
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ threesix.ai │
|
|
│ │
|
|
│ git.threesix.ai ──────▶ gitea (web UI + SSH :22) │
|
|
│ registry.threesix.ai ─▶ zot (internal only, HTTPS for UI) │
|
|
│ ci.threesix.ai ───────▶ woodpecker (web UI) │
|
|
│ *.threesix.ai ────────▶ project deployments │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ k3s cluster │
|
|
│ │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ gitea │───▶│ woodpecker │───▶│ zot │ │
|
|
│ │ (git repos) │ │ (CI/CD) │ │ (registry) │ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
│ │ │ │ │
|
|
│ │ ▼ │ │
|
|
│ │ ┌──────────────┐ │ │
|
|
│ └───────────▶│ rdev-api │◀──────────┘ │
|
|
│ │ │ │
|
|
│ │ - Create repos │
|
|
│ │ - Deploy apps │
|
|
│ │ - Manage DNS │
|
|
│ └──────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌──────────────┐ │
|
|
│ │ Cloudflare │ │
|
|
│ │ DNS API │ │
|
|
│ └──────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### Credentials (from .secrets)
|
|
|
|
| Key | Value | Purpose |
|
|
|-----|-------|---------|
|
|
| CLOUDFLARE_API_TOKEN | `nGoDhG6Za...` | DNS management |
|
|
| CLOUDFLARE_ZONE_ID | `e0bc8d51...` | threesix.ai zone |
|
|
| GITEA_ADMIN_PASSWORD | (generate) | Gitea admin login |
|
|
| GITEA_SECRET_KEY | (generate: `openssl rand -hex 32`) | Gitea internal security |
|
|
| GITEA_API_TOKEN | (create in Gitea UI) | rdev-api access to Gitea |
|
|
| GITEA_OAUTH_CLIENT_ID | (create in Gitea UI) | Woodpecker OAuth |
|
|
| GITEA_OAUTH_CLIENT_SECRET | (create in Gitea UI) | Woodpecker OAuth |
|
|
| WOODPECKER_AGENT_SECRET | (generate: `openssl rand -hex 32`) | Agent-server auth |
|
|
|
|
### Network
|
|
|
|
| Resource | Value |
|
|
|----------|-------|
|
|
| External IP | 208.122.204.172 |
|
|
| Let's Encrypt Email | jordan@threesix.ai |
|
|
| Domain | threesix.ai |
|
|
|
|
### Admin Access
|
|
|
|
Admin SSH key (add in Gitea UI: Settings → SSH/GPG Keys):
|
|
```
|
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDZwQF0Ro0E0foFo0oro/NrfUb5abEec/A0OP2qO8dVn jordanwashburn@jordanmacstudio.lan
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 1: Foundation (K8s Infrastructure)
|
|
|
|
### 1.1 Create Namespace and Secrets
|
|
|
|
```yaml
|
|
# deployments/k8s/base/threesix/namespace.yaml
|
|
apiVersion: v1
|
|
kind: Namespace
|
|
metadata:
|
|
name: threesix
|
|
---
|
|
# Cloudflare API secret for cert-manager and rdev-api
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: cloudflare-api
|
|
namespace: threesix
|
|
type: Opaque
|
|
stringData:
|
|
api-token: "${CLOUDFLARE_API_TOKEN}"
|
|
zone-id: "${CLOUDFLARE_ZONE_ID}"
|
|
```
|
|
|
|
### 1.2 Configure cert-manager for Wildcard Certs
|
|
|
|
```yaml
|
|
# deployments/k8s/base/threesix/cluster-issuer.yaml
|
|
apiVersion: cert-manager.io/v1
|
|
kind: ClusterIssuer
|
|
metadata:
|
|
name: letsencrypt-threesix
|
|
spec:
|
|
acme:
|
|
server: https://acme-v02.api.letsencrypt.org/directory
|
|
email: jordan@threesix.ai
|
|
privateKeySecretRef:
|
|
name: letsencrypt-threesix-account
|
|
solvers:
|
|
- dns01:
|
|
cloudflare:
|
|
apiTokenSecretRef:
|
|
name: cloudflare-api
|
|
key: api-token
|
|
selector:
|
|
dnsZones:
|
|
- "threesix.ai"
|
|
---
|
|
# Wildcard certificate
|
|
apiVersion: cert-manager.io/v1
|
|
kind: Certificate
|
|
metadata:
|
|
name: threesix-wildcard
|
|
namespace: threesix
|
|
spec:
|
|
secretName: threesix-wildcard-tls
|
|
issuerRef:
|
|
name: letsencrypt-threesix
|
|
kind: ClusterIssuer
|
|
dnsNames:
|
|
- "threesix.ai"
|
|
- "*.threesix.ai"
|
|
```
|
|
|
|
### 1.3 Deploy Gitea
|
|
|
|
```yaml
|
|
# deployments/k8s/base/threesix/gitea.yaml
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: gitea-admin
|
|
namespace: threesix
|
|
type: Opaque
|
|
stringData:
|
|
username: jordan
|
|
password: "${GITEA_ADMIN_PASSWORD}"
|
|
---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: gitea-config
|
|
namespace: threesix
|
|
data:
|
|
app.ini: |
|
|
APP_NAME = threesix
|
|
RUN_MODE = prod
|
|
|
|
[server]
|
|
DOMAIN = git.threesix.ai
|
|
SSH_DOMAIN = git.threesix.ai
|
|
ROOT_URL = https://git.threesix.ai/
|
|
HTTP_PORT = 3000
|
|
SSH_PORT = 22
|
|
SSH_LISTEN_PORT = 22
|
|
LFS_START_SERVER = true
|
|
|
|
[database]
|
|
DB_TYPE = sqlite3
|
|
PATH = /data/gitea/gitea.db
|
|
|
|
[repository]
|
|
ROOT = /data/git/repositories
|
|
DEFAULT_BRANCH = main
|
|
|
|
[security]
|
|
INSTALL_LOCK = true
|
|
SECRET_KEY = ${GITEA_SECRET_KEY}
|
|
|
|
[service]
|
|
DISABLE_REGISTRATION = true
|
|
REQUIRE_SIGNIN_VIEW = false
|
|
|
|
[oauth2_client]
|
|
ENABLE_AUTO_REGISTRATION = false
|
|
|
|
[webhook]
|
|
ALLOWED_HOST_LIST = woodpecker-server.threesix.svc.cluster.local
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: StatefulSet
|
|
metadata:
|
|
name: gitea
|
|
namespace: threesix
|
|
spec:
|
|
serviceName: gitea
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app: gitea
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: gitea
|
|
spec:
|
|
initContainers:
|
|
- name: init-config
|
|
image: gitea/gitea:latest
|
|
command: ['sh', '-c', 'cp /etc/gitea/app.ini /data/gitea/conf/app.ini']
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /data
|
|
- name: config
|
|
mountPath: /etc/gitea
|
|
containers:
|
|
- name: gitea
|
|
image: gitea/gitea:latest
|
|
ports:
|
|
- containerPort: 3000
|
|
name: http
|
|
- containerPort: 22
|
|
name: ssh
|
|
env:
|
|
- name: GITEA_ADMIN_USERNAME
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: gitea-admin
|
|
key: username
|
|
- name: GITEA_ADMIN_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: gitea-admin
|
|
key: password
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /data
|
|
resources:
|
|
requests:
|
|
memory: "256Mi"
|
|
cpu: "100m"
|
|
limits:
|
|
memory: "1Gi"
|
|
cpu: "1000m"
|
|
volumes:
|
|
- name: config
|
|
configMap:
|
|
name: gitea-config
|
|
volumeClaimTemplates:
|
|
- metadata:
|
|
name: data
|
|
spec:
|
|
accessModes: ["ReadWriteOnce"]
|
|
storageClassName: longhorn
|
|
resources:
|
|
requests:
|
|
storage: 20Gi
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: gitea
|
|
namespace: threesix
|
|
spec:
|
|
selector:
|
|
app: gitea
|
|
ports:
|
|
- name: http
|
|
port: 3000
|
|
targetPort: 3000
|
|
- name: ssh
|
|
port: 22
|
|
targetPort: 22
|
|
---
|
|
# External SSH access via LoadBalancer
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: gitea-ssh
|
|
namespace: threesix
|
|
spec:
|
|
type: LoadBalancer
|
|
selector:
|
|
app: gitea
|
|
ports:
|
|
- name: ssh
|
|
port: 22
|
|
targetPort: 22
|
|
---
|
|
# HTTP access via Ingress
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: gitea
|
|
namespace: threesix
|
|
annotations:
|
|
cert-manager.io/cluster-issuer: letsencrypt-threesix
|
|
spec:
|
|
ingressClassName: traefik
|
|
tls:
|
|
- hosts:
|
|
- git.threesix.ai
|
|
secretName: git-threesix-tls
|
|
rules:
|
|
- host: git.threesix.ai
|
|
http:
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: gitea
|
|
port:
|
|
number: 3000
|
|
```
|
|
|
|
### 1.4 Deploy Zot Registry
|
|
|
|
```yaml
|
|
# deployments/k8s/base/threesix/zot.yaml
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: zot-config
|
|
namespace: threesix
|
|
data:
|
|
config.json: |
|
|
{
|
|
"distSpecVersion": "1.1.0",
|
|
"storage": {
|
|
"rootDirectory": "/var/lib/zot",
|
|
"gc": true,
|
|
"gcDelay": "1h"
|
|
},
|
|
"http": {
|
|
"address": "0.0.0.0",
|
|
"port": "5000"
|
|
},
|
|
"log": {
|
|
"level": "info"
|
|
},
|
|
"extensions": {
|
|
"search": {
|
|
"enable": true
|
|
},
|
|
"ui": {
|
|
"enable": true
|
|
}
|
|
}
|
|
}
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: StatefulSet
|
|
metadata:
|
|
name: zot
|
|
namespace: threesix
|
|
spec:
|
|
serviceName: zot
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app: zot
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: zot
|
|
spec:
|
|
containers:
|
|
- name: zot
|
|
image: ghcr.io/project-zot/zot-linux-amd64:latest
|
|
ports:
|
|
- containerPort: 5000
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /var/lib/zot
|
|
- name: config
|
|
mountPath: /etc/zot/config.json
|
|
subPath: config.json
|
|
resources:
|
|
requests:
|
|
memory: "128Mi"
|
|
cpu: "100m"
|
|
limits:
|
|
memory: "512Mi"
|
|
cpu: "1000m"
|
|
volumes:
|
|
- name: config
|
|
configMap:
|
|
name: zot-config
|
|
volumeClaimTemplates:
|
|
- metadata:
|
|
name: data
|
|
spec:
|
|
accessModes: ["ReadWriteOnce"]
|
|
storageClassName: longhorn
|
|
resources:
|
|
requests:
|
|
storage: 50Gi
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: zot
|
|
namespace: threesix
|
|
spec:
|
|
selector:
|
|
app: zot
|
|
ports:
|
|
- port: 5000
|
|
targetPort: 5000
|
|
---
|
|
# Internal DNS name for cluster access
|
|
# Pods can pull from: zot.threesix.svc.cluster.local:5000/image:tag
|
|
---
|
|
# Optional: External UI access
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: zot
|
|
namespace: threesix
|
|
annotations:
|
|
cert-manager.io/cluster-issuer: letsencrypt-threesix
|
|
spec:
|
|
ingressClassName: traefik
|
|
tls:
|
|
- hosts:
|
|
- registry.threesix.ai
|
|
secretName: registry-threesix-tls
|
|
rules:
|
|
- host: registry.threesix.ai
|
|
http:
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: zot
|
|
port:
|
|
number: 5000
|
|
```
|
|
|
|
### 1.5 Initial DNS Records
|
|
|
|
Create via Cloudflare API or dashboard:
|
|
|
|
| Type | Name | Value | Proxy |
|
|
|------|------|-------|-------|
|
|
| A | git | 208.122.204.172 | No (SSH needs direct) |
|
|
| A | registry | 208.122.204.172 | No |
|
|
| A | ci | 208.122.204.172 | Yes (optional) |
|
|
| A | * | 208.122.204.172 | Yes (optional) |
|
|
|
|
---
|
|
|
|
## Phase 2: CI/CD (Woodpecker)
|
|
|
|
### 2.1 Create Gitea OAuth Application
|
|
|
|
Before deploying Woodpecker, create an OAuth application in Gitea:
|
|
|
|
1. Login to https://git.threesix.ai as admin
|
|
2. Go to Site Administration → Applications → Create OAuth2 Application
|
|
3. Application Name: `Woodpecker CI`
|
|
4. Redirect URI: `https://ci.threesix.ai/authorize`
|
|
5. Save the Client ID and Client Secret
|
|
|
|
### 2.2 Deploy Woodpecker Server
|
|
|
|
```yaml
|
|
# deployments/k8s/base/threesix/woodpecker-server.yaml
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: woodpecker-secrets
|
|
namespace: threesix
|
|
type: Opaque
|
|
stringData:
|
|
# Generate with: openssl rand -hex 32
|
|
WOODPECKER_AGENT_SECRET: "${WOODPECKER_AGENT_SECRET}"
|
|
# From Gitea OAuth application
|
|
WOODPECKER_GITEA_CLIENT: "${GITEA_OAUTH_CLIENT_ID}"
|
|
WOODPECKER_GITEA_SECRET: "${GITEA_OAUTH_CLIENT_SECRET}"
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: woodpecker-server
|
|
namespace: threesix
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app: woodpecker-server
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: woodpecker-server
|
|
spec:
|
|
containers:
|
|
- name: woodpecker
|
|
image: woodpeckerci/woodpecker-server:latest
|
|
ports:
|
|
- containerPort: 8000
|
|
env:
|
|
- name: WOODPECKER_HOST
|
|
value: "https://ci.threesix.ai"
|
|
- name: WOODPECKER_OPEN
|
|
value: "true"
|
|
- name: WOODPECKER_ADMIN
|
|
value: "jordan"
|
|
# Gitea forge integration
|
|
- name: WOODPECKER_GITEA
|
|
value: "true"
|
|
- name: WOODPECKER_GITEA_URL
|
|
value: "https://git.threesix.ai"
|
|
envFrom:
|
|
- secretRef:
|
|
name: woodpecker-secrets
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /var/lib/woodpecker
|
|
volumes:
|
|
- name: data
|
|
persistentVolumeClaim:
|
|
claimName: woodpecker-data
|
|
---
|
|
apiVersion: v1
|
|
kind: PersistentVolumeClaim
|
|
metadata:
|
|
name: woodpecker-data
|
|
namespace: threesix
|
|
spec:
|
|
accessModes: ["ReadWriteOnce"]
|
|
storageClassName: longhorn
|
|
resources:
|
|
requests:
|
|
storage: 5Gi
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: woodpecker-server
|
|
namespace: threesix
|
|
spec:
|
|
selector:
|
|
app: woodpecker-server
|
|
ports:
|
|
- name: http
|
|
port: 8000
|
|
targetPort: 8000
|
|
- name: grpc
|
|
port: 9000
|
|
targetPort: 9000
|
|
---
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: woodpecker
|
|
namespace: threesix
|
|
annotations:
|
|
cert-manager.io/cluster-issuer: letsencrypt-threesix
|
|
spec:
|
|
ingressClassName: traefik
|
|
tls:
|
|
- hosts:
|
|
- ci.threesix.ai
|
|
secretName: ci-threesix-tls
|
|
rules:
|
|
- host: ci.threesix.ai
|
|
http:
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: woodpecker-server
|
|
port:
|
|
number: 8000
|
|
```
|
|
|
|
### 2.3 Deploy Woodpecker Agent (with Kaniko)
|
|
|
|
```yaml
|
|
# deployments/k8s/base/threesix/woodpecker-agent.yaml
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: woodpecker-agent
|
|
namespace: threesix
|
|
spec:
|
|
replicas: 2
|
|
selector:
|
|
matchLabels:
|
|
app: woodpecker-agent
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: woodpecker-agent
|
|
spec:
|
|
containers:
|
|
- name: agent
|
|
image: woodpeckerci/woodpecker-agent:latest
|
|
env:
|
|
- name: WOODPECKER_SERVER
|
|
value: "woodpecker-server.threesix.svc:9000"
|
|
- name: WOODPECKER_BACKEND
|
|
value: "kubernetes"
|
|
- name: WOODPECKER_BACKEND_K8S_NAMESPACE
|
|
value: "threesix"
|
|
- name: WOODPECKER_BACKEND_K8S_STORAGE_CLASS
|
|
value: "longhorn"
|
|
- name: WOODPECKER_BACKEND_K8S_VOLUME_SIZE
|
|
value: "10Gi"
|
|
envFrom:
|
|
- secretRef:
|
|
name: woodpecker-secrets
|
|
serviceAccountName: woodpecker-agent
|
|
---
|
|
apiVersion: v1
|
|
kind: ServiceAccount
|
|
metadata:
|
|
name: woodpecker-agent
|
|
namespace: threesix
|
|
---
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: Role
|
|
metadata:
|
|
name: woodpecker-agent
|
|
namespace: threesix
|
|
rules:
|
|
- apiGroups: [""]
|
|
resources: ["pods", "pods/log", "secrets", "configmaps", "persistentvolumeclaims"]
|
|
verbs: ["*"]
|
|
---
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: RoleBinding
|
|
metadata:
|
|
name: woodpecker-agent
|
|
namespace: threesix
|
|
roleRef:
|
|
apiGroup: rbac.authorization.k8s.io
|
|
kind: Role
|
|
name: woodpecker-agent
|
|
subjects:
|
|
- kind: ServiceAccount
|
|
name: woodpecker-agent
|
|
namespace: threesix
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 3: rdev-api Extensions
|
|
|
|
### 3.1 New Port Interfaces
|
|
|
|
```go
|
|
// internal/port/git.go
|
|
package port
|
|
|
|
import "context"
|
|
|
|
// GitRepository manages git repositories via Gitea API.
|
|
type GitRepository interface {
|
|
// CreateRepo creates a new git repository.
|
|
CreateRepo(ctx context.Context, name, description string, private bool) (*Repo, error)
|
|
|
|
// DeleteRepo deletes a repository.
|
|
DeleteRepo(ctx context.Context, owner, name string) error
|
|
|
|
// ListRepos returns all repositories for an owner.
|
|
ListRepos(ctx context.Context, owner string) ([]*Repo, error)
|
|
|
|
// GetRepo returns a single repository.
|
|
GetRepo(ctx context.Context, owner, name string) (*Repo, error)
|
|
|
|
// AddCollaborator adds a user as collaborator to a repo.
|
|
AddCollaborator(ctx context.Context, owner, repo, username string, permission string) error
|
|
|
|
// AddDeployKey adds a deploy key to a repo for read-only or read-write access.
|
|
AddDeployKey(ctx context.Context, owner, repo, title, publicKey string, readOnly bool) error
|
|
|
|
// CreateWebhook creates a webhook to trigger on push events.
|
|
CreateWebhook(ctx context.Context, owner, repo, url, secret string, events []string) error
|
|
}
|
|
|
|
// Repo represents a git repository.
|
|
type Repo struct {
|
|
ID int64
|
|
Owner string
|
|
Name string
|
|
FullName string // owner/name
|
|
Description string
|
|
Private bool
|
|
CloneSSH string // git@git.threesix.ai:owner/name.git
|
|
CloneHTTP string // https://git.threesix.ai/owner/name.git
|
|
HTMLURL string // https://git.threesix.ai/owner/name
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
```
|
|
|
|
```go
|
|
// internal/port/dns.go
|
|
package port
|
|
|
|
import "context"
|
|
|
|
// DNSProvider manages DNS records.
|
|
type DNSProvider interface {
|
|
// CreateRecord creates a DNS record.
|
|
CreateRecord(ctx context.Context, record DNSRecord) error
|
|
|
|
// DeleteRecord removes a DNS record.
|
|
DeleteRecord(ctx context.Context, recordType, name string) error
|
|
|
|
// ListRecords returns all records for the zone.
|
|
ListRecords(ctx context.Context) ([]*DNSRecord, error)
|
|
}
|
|
|
|
// DNSRecord represents a DNS record.
|
|
type DNSRecord struct {
|
|
Type string // A, CNAME, TXT
|
|
Name string // subdomain or @ for root
|
|
Content string // IP or target
|
|
TTL int // seconds, 1 = auto
|
|
Proxied bool // Cloudflare proxy
|
|
}
|
|
```
|
|
|
|
```go
|
|
// internal/port/deployer.go
|
|
package port
|
|
|
|
import "context"
|
|
|
|
// Deployer manages application deployments.
|
|
type Deployer interface {
|
|
// Deploy creates or updates a deployment.
|
|
Deploy(ctx context.Context, spec DeploySpec) error
|
|
|
|
// Undeploy removes a deployment.
|
|
Undeploy(ctx context.Context, projectName string) error
|
|
|
|
// GetStatus returns deployment status.
|
|
GetStatus(ctx context.Context, projectName string) (*DeployStatus, error)
|
|
}
|
|
|
|
// DeploySpec defines a deployment.
|
|
type DeploySpec struct {
|
|
ProjectName string
|
|
Image string
|
|
Domain string // e.g., "myapp.threesix.ai"
|
|
Port int // container port
|
|
Replicas int
|
|
EnvVars map[string]string
|
|
Secrets map[string]string
|
|
}
|
|
|
|
// DeployStatus represents current deployment state.
|
|
type DeployStatus struct {
|
|
ProjectName string
|
|
Image string
|
|
Replicas int
|
|
ReadyReplicas int
|
|
URL string
|
|
Status string // "running", "pending", "failed"
|
|
}
|
|
```
|
|
|
|
### 3.2 New Adapters
|
|
|
|
```
|
|
internal/adapter/
|
|
├── gitea/ # Gitea REST API client
|
|
│ └── client.go
|
|
├── cloudflare/ # Cloudflare DNS API client
|
|
│ └── client.go
|
|
├── deployer/ # K8s deployment manager
|
|
│ └── deployer.go
|
|
└── registry/ # Zot registry client (optional)
|
|
└── client.go
|
|
```
|
|
|
|
#### Gitea Client Example
|
|
|
|
```go
|
|
// internal/adapter/gitea/client.go
|
|
package gitea
|
|
|
|
import (
|
|
"code.gitea.io/sdk/gitea"
|
|
"github.com/orchard9/rdev/internal/port"
|
|
)
|
|
|
|
type Client struct {
|
|
client *gitea.Client
|
|
owner string // default owner/org for repos
|
|
}
|
|
|
|
func NewClient(url, token, defaultOwner string) (*Client, error) {
|
|
client, err := gitea.NewClient(url, gitea.SetToken(token))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Client{client: client, owner: defaultOwner}, nil
|
|
}
|
|
|
|
func (c *Client) CreateRepo(ctx context.Context, name, description string, private bool) (*port.Repo, error) {
|
|
repo, _, err := c.client.CreateOrgRepo(c.owner, gitea.CreateRepoOption{
|
|
Name: name,
|
|
Description: description,
|
|
Private: private,
|
|
AutoInit: true,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &port.Repo{
|
|
ID: repo.ID,
|
|
Owner: repo.Owner.UserName,
|
|
Name: repo.Name,
|
|
FullName: repo.FullName,
|
|
Description: repo.Description,
|
|
Private: repo.Private,
|
|
CloneSSH: repo.SSHURL,
|
|
CloneHTTP: repo.CloneURL,
|
|
HTMLURL: repo.HTMLURL,
|
|
CreatedAt: repo.Created,
|
|
UpdatedAt: repo.Updated,
|
|
}, nil
|
|
}
|
|
```
|
|
|
|
### 3.3 New Handlers
|
|
|
|
```go
|
|
// internal/handlers/projects_git.go
|
|
|
|
// POST /projects/{id}/repo - Create git repo for project
|
|
// DELETE /projects/{id}/repo - Delete git repo
|
|
// GET /projects/{id}/repo - Get repo info
|
|
|
|
// POST /projects/{id}/deploy - Deploy project
|
|
// DELETE /projects/{id}/deploy - Undeploy project
|
|
// GET /projects/{id}/deploy/status - Get deployment status
|
|
|
|
// POST /projects/{id}/domain - Set custom domain
|
|
// DELETE /projects/{id}/domain - Remove custom domain
|
|
```
|
|
|
|
### 3.4 New API Endpoints
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| POST | `/projects/{id}/repo` | Create git repo |
|
|
| DELETE | `/projects/{id}/repo` | Delete git repo |
|
|
| GET | `/projects/{id}/repo` | Get repo info (clone URLs) |
|
|
| POST | `/projects/{id}/deploy` | Deploy from image |
|
|
| DELETE | `/projects/{id}/deploy` | Remove deployment |
|
|
| GET | `/projects/{id}/deploy/status` | Deployment status |
|
|
| POST | `/projects/{id}/domain` | Add custom domain |
|
|
| DELETE | `/projects/{id}/domain` | Remove custom domain |
|
|
|
|
---
|
|
|
|
## Phase 4: Database Schema
|
|
|
|
### 4.1 Migration: Add Git and Deployment Fields
|
|
|
|
```sql
|
|
-- migrations/010_project_infrastructure.up.sql
|
|
|
|
-- Add infrastructure fields to projects
|
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS
|
|
git_repo_name VARCHAR(255);
|
|
|
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS
|
|
git_clone_ssh VARCHAR(512);
|
|
|
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS
|
|
git_clone_http VARCHAR(512);
|
|
|
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS
|
|
domain VARCHAR(255);
|
|
|
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS
|
|
custom_domain VARCHAR(255);
|
|
|
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS
|
|
deployment_image VARCHAR(512);
|
|
|
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS
|
|
deployment_status VARCHAR(50) DEFAULT 'none';
|
|
|
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS
|
|
deployment_replicas INTEGER DEFAULT 1;
|
|
|
|
-- Index for domain lookups
|
|
CREATE INDEX IF NOT EXISTS idx_projects_domain ON projects(domain);
|
|
CREATE INDEX IF NOT EXISTS idx_projects_custom_domain ON projects(custom_domain);
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5: Pantheon Integration
|
|
|
|
### 5.1 New Commands for Agents
|
|
|
|
```
|
|
/project create <name>
|
|
→ Creates project in DB
|
|
→ Creates git repo in Gitea (threesix/<name>)
|
|
→ Activates repo in Woodpecker CI
|
|
→ Creates DNS record (<name>.threesix.ai)
|
|
→ Returns clone URLs:
|
|
SSH: git@git.threesix.ai:threesix/<name>.git
|
|
HTTPS: https://git.threesix.ai/threesix/<name>.git
|
|
|
|
/project deploy <name>
|
|
→ Triggers build from latest commit
|
|
→ Deploys to k8s
|
|
→ Returns live URL
|
|
|
|
/project status <name>
|
|
→ Shows git repo, deployment status, URLs
|
|
|
|
/project domain <name> <custom-domain>
|
|
→ Adds custom domain to project
|
|
→ Instructions for DNS pointing
|
|
```
|
|
|
|
### 5.2 Webhook Flow
|
|
|
|
```
|
|
Agent pushes code to Gitea
|
|
│
|
|
▼
|
|
Gitea receives push, fires webhook to Woodpecker
|
|
│
|
|
▼
|
|
Woodpecker reads .woodpecker.yml from repo
|
|
│
|
|
▼
|
|
Kaniko builds image, pushes to zot
|
|
│
|
|
▼
|
|
Woodpecker calls rdev-api: POST /projects/{id}/deploy
|
|
│
|
|
▼
|
|
rdev-api creates/updates K8s Deployment + Ingress
|
|
│
|
|
▼
|
|
Project live at https://{name}.threesix.ai
|
|
```
|
|
|
|
**Note:** When you activate a repo in Woodpecker's UI, it automatically creates the webhook in Gitea via OAuth. No manual webhook configuration needed.
|
|
|
|
---
|
|
|
|
## Implementation Checklist
|
|
|
|
### Phase 1: Foundation ✅ COMPLETED (2026-01-26)
|
|
- [x] Create `threesix` namespace
|
|
- [x] Create Cloudflare API secret
|
|
- [x] Configure Issuer for DNS-01 challenge (namespace-scoped, not ClusterIssuer)
|
|
- [x] Request wildcard certificate (*.threesix.ai)
|
|
- [x] Deploy Gitea StatefulSet (rootless image, PostgreSQL backend, writable config)
|
|
- [x] Configure Gitea LoadBalancer for SSH (208.122.204.172:22)
|
|
- [x] Deploy Zot registry (10Gi storage)
|
|
- [x] Create DNS records: git.threesix.ai, registry.threesix.ai → 208.122.204.172
|
|
- [x] Test: `https://git.threesix.ai` shows Gitea UI ✅
|
|
- [x] Test: `https://registry.threesix.ai/v2/_catalog` returns `{"repositories":[]}` ✅
|
|
- [x] Complete Gitea installation wizard ✅
|
|
|
|
**Implementation Notes:**
|
|
- Used namespace-scoped `Issuer` instead of `ClusterIssuer` (cert-manager couldn't access secrets across namespaces)
|
|
- Gitea uses PostgreSQL (`postgres.databases.svc.cluster.local`) instead of SQLite
|
|
- Gitea credentials: `gitea` user, password in `/tmp/gitea-db-password.txt`
|
|
- Rootless Gitea image requires `securityContext.fsGroup: 1000` and writable `/etc/gitea` via volume subPath
|
|
|
|
### Phase 2: CI/CD ✅ COMPLETED (2026-01-26)
|
|
- [x] Create Gitea OAuth application for Woodpecker
|
|
- [x] Generate Woodpecker agent secret
|
|
- [x] Create DNS record: ci.threesix.ai → 208.122.204.172
|
|
- [x] Deploy Woodpecker server with Gitea forge
|
|
- [x] Deploy Woodpecker agents (2 replicas, K8s backend)
|
|
- [x] TLS certificate issued for ci.threesix.ai
|
|
- [ ] Test: Login to Woodpecker via Gitea OAuth
|
|
- [ ] Test: Activate repo in Woodpecker (auto-creates webhook)
|
|
- [ ] Test: Push triggers build
|
|
- [ ] Test: Kaniko builds and pushes to Zot
|
|
|
|
**Secrets saved:**
|
|
- Agent secret: `/tmp/woodpecker-agent-secret.txt`
|
|
- Gitea OAuth: Client ID `7548afec-43e0-486a-b6eb-e2a7d5c88d41`
|
|
|
|
### Phase 3: rdev-api ✅ COMPLETED (2026-01-26)
|
|
- [x] Add GitRepository port interface (`internal/port/git_repository.go`)
|
|
- [x] Add DNSProvider port interface (`internal/port/dns_provider.go`)
|
|
- [x] Add Deployer port interface (`internal/port/deployer.go`)
|
|
- [x] Implement Gitea adapter (`internal/adapter/gitea/client.go`) using `code.gitea.io/sdk/gitea`
|
|
- [x] Implement Cloudflare adapter (`internal/adapter/cloudflare/client.go`)
|
|
- [x] Implement K8s deployer adapter (`internal/adapter/deployer/deployer.go`)
|
|
- [x] Add database migration (`internal/db/migrations/008_project_infrastructure.sql`)
|
|
- [x] Add infrastructure handler (`internal/handlers/infrastructure.go`)
|
|
- [x] Wire up in main.go with environment variables
|
|
- [ ] Test: API can create repos
|
|
- [ ] Test: API can manage DNS
|
|
- [ ] Test: API can deploy apps
|
|
|
|
**New Environment Variables:**
|
|
```
|
|
GITEA_URL=https://git.threesix.ai
|
|
GITEA_TOKEN=<from Gitea UI: Settings → Applications → Access Tokens>
|
|
GITEA_DEFAULT_ORG=threesix
|
|
CLOUDFLARE_API_TOKEN=<existing>
|
|
CLOUDFLARE_ZONE_ID=<existing>
|
|
DEFAULT_DOMAIN=threesix.ai
|
|
DEPLOY_NAMESPACE=projects
|
|
DEPLOY_TLS_ISSUER=letsencrypt-threesix
|
|
CLUSTER_IP=208.122.204.172
|
|
```
|
|
|
|
**New API Endpoints:**
|
|
- `POST /projects/{id}/repo` - Create git repo
|
|
- `GET /projects/{id}/repo` - Get repo info
|
|
- `DELETE /projects/{id}/repo` - Delete repo
|
|
- `POST /projects/{id}/deploy` - Deploy from image
|
|
- `GET /projects/{id}/deploy/status` - Get deployment status
|
|
- `DELETE /projects/{id}/deploy` - Undeploy
|
|
- `POST /projects/{id}/deploy/restart` - Restart deployment
|
|
- `POST /projects/{id}/deploy/scale` - Scale replicas
|
|
- `GET /projects/{id}/deploy/logs` - Get logs
|
|
- `POST /projects/{id}/domain` - Add custom domain
|
|
- `DELETE /projects/{id}/domain` - Remove domain
|
|
|
|
### Phase 4: Integration ✅ COMPLETED (2026-01-26)
|
|
- [x] Wire up webhook: build → deploy (`internal/handlers/woodpecker_webhook.go`)
|
|
- [x] Add project commands to Pantheon (`.claude/commands/project-*.md`)
|
|
- [x] Create project infrastructure service (`internal/service/project_infra.go`)
|
|
- [x] Create project management handler (`internal/handlers/project_management.go`)
|
|
- [x] Update ExternalSecret with infrastructure credentials
|
|
- [x] Create .woodpecker.yml pipeline template
|
|
- [ ] Test: Login to Woodpecker via Gitea OAuth
|
|
- [ ] Test: end-to-end "create project" → "push code" → "live site"
|
|
|
|
**New Files Created:**
|
|
- `internal/handlers/woodpecker_webhook.go` - Handles Woodpecker CI webhooks for auto-deployment
|
|
- `internal/service/project_infra.go` - Orchestrates full project lifecycle (DB → Gitea → DNS → K8s)
|
|
- `internal/handlers/project_management.go` - HTTP handlers for project CRUD operations
|
|
- `.claude/commands/project-create.md` - /project create command
|
|
- `.claude/commands/project-status.md` - /project status command
|
|
- `.claude/commands/project-deploy.md` - /project deploy command
|
|
- `.claude/commands/project-list.md` - /project list command
|
|
|
|
**New API Endpoints:**
|
|
- `POST /project` - Create new project with git repo and DNS
|
|
- `GET /project` - List all projects with status
|
|
- `GET /project/{name}` - Get single project status
|
|
- `DELETE /project/{name}` - Delete project and all resources
|
|
- `POST /webhook/woodpecker` - Woodpecker CI webhook (auto-deploy on build success)
|
|
|
|
**Environment Variables Added:**
|
|
```
|
|
REGISTRY_URL=zot.threesix.svc.cluster.local:5000
|
|
WOODPECKER_WEBHOOK_SECRET=<from GCP Secret Manager>
|
|
```
|
|
|
|
### Phase 5: Polish
|
|
- [ ] Custom domain support
|
|
- [ ] Build notifications to Pantheon
|
|
- [ ] Deployment logs streaming
|
|
- [ ] Resource limits per project
|
|
- [ ] Usage metrics
|
|
|
|
---
|
|
|
|
## Resource Estimates
|
|
|
|
| Component | CPU Request | Memory Request | Storage |
|
|
|-----------|-------------|----------------|---------|
|
|
| Gitea | 100m | 256Mi | 20Gi |
|
|
| Zot | 100m | 128Mi | 50Gi |
|
|
| Woodpecker Server | 100m | 128Mi | 5Gi |
|
|
| Woodpecker Agent (x2) | 200m each | 256Mi each | - |
|
|
| **Total** | ~700m | ~1Gi | 75Gi |
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
1. **Gitea admin** - Registration disabled, only admin user can create accounts
|
|
2. **Gitea API token** - Create a dedicated token for rdev-api with repo scope
|
|
3. **Registry access** - Internal only, no auth needed (ClusterIP)
|
|
4. **Woodpecker** - OAuth via Gitea, inherits Gitea permissions
|
|
5. **Cloudflare token** - Scoped to DNS edit only
|
|
6. **Deploy permissions** - rdev-api ServiceAccount limited to `threesix` and `projects` namespaces
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. Review this plan
|
|
2. I deploy Phase 1 infrastructure
|
|
3. Test git and registry
|
|
4. Deploy Phase 2 CI/CD
|
|
5. Implement Phase 3 rdev-api changes
|
|
6. Integration testing
|
|
7. Pantheon integration
|