diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml index 6add55f..22b77ca 100644 --- a/.forgejo/workflows/deploy.yaml +++ b/.forgejo/workflows/deploy.yaml @@ -67,24 +67,67 @@ jobs: # Injeta variáveis (Lembre-se de mudar DATABASE_URL para sslmode=disable no Forgejo!) kubectl delete secret backend-secrets -n gohorsejobsdev --ignore-not-found - kubectl create secret generic backend-secrets -n gohorsejobsdev \ - --from-literal=MTU="${{ vars.MTU }}" \ - --from-literal=DATABASE_URL="${{ vars.DATABASE_URL }}" \ - --from-literal=AMQP_URL="${{ vars.AMQP_URL }}" \ - --from-literal=JWT_SECRET="${{ vars.JWT_SECRET }}" \ - --from-literal=JWT_EXPIRATION="${{ vars.JWT_EXPIRATION }}" \ - --from-literal=PASSWORD_PEPPER="${{ vars.PASSWORD_PEPPER }}" \ - --from-literal=COOKIE_SECRET="${{ vars.COOKIE_SECRET }}" \ - --from-literal=COOKIE_DOMAIN="${{ vars.COOKIE_DOMAIN }}" \ - --from-literal=BACKEND_PORT="${{ vars.BACKEND_PORT }}" \ - --from-literal=BACKEND_HOST="${{ vars.BACKEND_HOST }}" \ - --from-literal=ENV="${{ vars.ENV }}" \ - --from-literal=CORS_ORIGINS="${{ vars.CORS_ORIGINS }}" \ - --from-literal=S3_BUCKET="${{ vars.S3_BUCKET }}" \ - --from-literal=AWS_REGION="${{ vars.AWS_REGION }}" \ - --from-literal=AWS_ENDPOINT="${{ vars.AWS_ENDPOINT }}" \ - --from-literal=AWS_ACCESS_KEY_ID="${{ vars.AWS_ACCESS_KEY_ID }}" \ - --from-literal=AWS_SECRET_ACCESS_KEY="${{ vars.AWS_SECRET_ACCESS_KEY }}" + + # Prepare RSA key file if available (prefer secrets over vars) + if [ -n "${{ secrets.RSA_PRIVATE_KEY_BASE64 }}" ]; then + echo "Decoding RSA_PRIVATE_KEY_BASE64 from secrets" + printf '%b' "${{ secrets.RSA_PRIVATE_KEY_BASE64 }}" > /tmp/rsa_key.pem || true + # if it's base64-encoded PEM, decode it + if base64 -d /tmp/rsa_key.pem >/dev/null 2>&1; then + base64 -d /tmp/rsa_key.pem > /tmp/rsa_key_decoded.pem && mv /tmp/rsa_key_decoded.pem /tmp/rsa_key.pem || true + fi + elif [ -n "${{ vars.RSA_PRIVATE_KEY_BASE64 }}" ]; then + echo "Decoding RSA_PRIVATE_KEY_BASE64 from vars" + printf '%b' "${{ vars.RSA_PRIVATE_KEY_BASE64 }}" > /tmp/rsa_key.pem || true + if base64 -d /tmp/rsa_key.pem >/dev/null 2>&1; then + base64 -d /tmp/rsa_key.pem > /tmp/rsa_key_decoded.pem && mv /tmp/rsa_key_decoded.pem /tmp/rsa_key.pem || true + fi + fi + + # Create secret: if rsa file exists, create secret from file (robust); otherwise fallback to from-literal + if [ -f /tmp/rsa_key.pem ]; then + kubectl create secret generic backend-secrets -n gohorsejobsdev \ + --from-literal=MTU="${{ vars.MTU }}" \ + --from-literal=DATABASE_URL="${{ vars.DATABASE_URL }}" \ + --from-literal=AMQP_URL="${{ vars.AMQP_URL }}" \ + --from-literal=JWT_SECRET="${{ vars.JWT_SECRET }}" \ + --from-literal=JWT_EXPIRATION="${{ vars.JWT_EXPIRATION }}" \ + --from-literal=PASSWORD_PEPPER="${{ vars.PASSWORD_PEPPER }}" \ + --from-literal=COOKIE_SECRET="${{ vars.COOKIE_SECRET }}" \ + --from-literal=COOKIE_DOMAIN="${{ vars.COOKIE_DOMAIN }}" \ + --from-literal=BACKEND_PORT="${{ vars.BACKEND_PORT }}" \ + --from-literal=BACKEND_HOST="${{ vars.BACKEND_HOST }}" \ + --from-literal=ENV="${{ vars.ENV }}" \ + --from-literal=CORS_ORIGINS="${{ vars.CORS_ORIGINS }}" \ + --from-literal=S3_BUCKET="${{ vars.S3_BUCKET }}" \ + --from-literal=AWS_REGION="${{ vars.AWS_REGION }}" \ + --from-literal=AWS_ENDPOINT="${{ vars.AWS_ENDPOINT }}" \ + --from-literal=AWS_ACCESS_KEY_ID="${{ vars.AWS_ACCESS_KEY_ID }}" \ + --from-literal=AWS_SECRET_ACCESS_KEY="${{ vars.AWS_SECRET_ACCESS_KEY }}" \ + --from-file=private_key.pem=/tmp/rsa_key.pem \ + --dry-run=client -o yaml | kubectl apply -f - + else + kubectl create secret generic backend-secrets -n gohorsejobsdev \ + --from-literal=MTU="${{ vars.MTU }}" \ + --from-literal=DATABASE_URL="${{ vars.DATABASE_URL }}" \ + --from-literal=AMQP_URL="${{ vars.AMQP_URL }}" \ + --from-literal=JWT_SECRET="${{ vars.JWT_SECRET }}" \ + --from-literal=JWT_EXPIRATION="${{ vars.JWT_EXPIRATION }}" \ + --from-literal=PASSWORD_PEPPER="${{ vars.PASSWORD_PEPPER }}" \ + --from-literal=COOKIE_SECRET="${{ vars.COOKIE_SECRET }}" \ + --from-literal=COOKIE_DOMAIN="${{ vars.COOKIE_DOMAIN }}" \ + --from-literal=BACKEND_PORT="${{ vars.BACKEND_PORT }}" \ + --from-literal=BACKEND_HOST="${{ vars.BACKEND_HOST }}" \ + --from-literal=ENV="${{ vars.ENV }}" \ + --from-literal=CORS_ORIGINS="${{ vars.CORS_ORIGINS }}" \ + --from-literal=S3_BUCKET="${{ vars.S3_BUCKET }}" \ + --from-literal=AWS_REGION="${{ vars.AWS_REGION }}" \ + --from-literal=AWS_ENDPOINT="${{ vars.AWS_ENDPOINT }}" \ + --from-literal=AWS_ACCESS_KEY_ID="${{ vars.AWS_ACCESS_KEY_ID }}" \ + --from-literal=AWS_SECRET_ACCESS_KEY="${{ vars.AWS_SECRET_ACCESS_KEY }}" \ + --from-literal=RSA_PRIVATE_KEY_BASE64="${{ vars.RSA_PRIVATE_KEY_BASE64 }}" \ + --dry-run=client -o yaml | kubectl apply -f - + fi - name: Deploy to K3s run: | diff --git a/.github/workflows/migrate.yml b/.github/workflows/migrate.yml new file mode 100644 index 0000000..8a1c1cd --- /dev/null +++ b/.github/workflows/migrate.yml @@ -0,0 +1,51 @@ +name: Validate RSA and Run Migrations + +on: + push: + branches: [ dev ] + workflow_dispatch: {} + +jobs: + migrate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Validate RSA_PRIVATE_KEY_BASE64 secret + env: + RSA_B64: ${{ secrets.RSA_PRIVATE_KEY_BASE64 }} + run: | + if [ -z "$RSA_B64" ]; then + echo "RSA_PRIVATE_KEY_BASE64 secret is missing. Add it in repository secrets." >&2 + exit 1 + fi + echo "$RSA_B64" > rsa_base64.txt + if ! base64 -d rsa_base64.txt > /tmp/key.pem 2>/dev/null; then + # try convert literal \n + printf '%b' "$RSA_B64" > /tmp/key.pem || true + fi + if ! openssl pkey -in /tmp/key.pem -noout -text >/dev/null 2>&1; then + echo "RSA private key is invalid" >&2 + exit 1 + fi + + - name: Validate DATABASE_URL secret + if: ${{ always() }} + run: | + if [ -z "${{ secrets.DATABASE_URL }}" ]; then + echo "DATABASE_URL secret is missing. Set up your DB connection string in secrets." >&2 + exit 1 + fi + + - name: Run migrations + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + run: | + cd backend + go run ./cmd/manual_migrate diff --git a/backend/cmd/manual_migrate/main.go b/backend/cmd/manual_migrate/main.go index c53e08a..ea6d12f 100644 --- a/backend/cmd/manual_migrate/main.go +++ b/backend/cmd/manual_migrate/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "sort" "strings" "github.com/joho/godotenv" @@ -33,60 +34,69 @@ func main() { } defer db.Close() - // List of migrations to run (in order) - migrations := []string{ - "024_create_external_services_credentials.sql", - "025_create_chat_tables.sql", - "026_create_system_settings.sql", - "027_create_email_system.sql", - "028_add_avatar_url_to_users.sql", + // Discover migrations directory from several probable locations + possibleDirs := []string{ + "migrations", + "backend/migrations", + "../migrations", + "/home/yamamoto/lab/gohorsejobs/backend/migrations", } - for _, migFile := range migrations { - log.Printf("Processing migration: %s", migFile) + var migrationsDir string + for _, d := range possibleDirs { + if fi, err := os.Stat(d); err == nil && fi.IsDir() { + migrationsDir = d + break + } + } - // Try multiple paths - paths := []string{ - "migrations/" + migFile, - "backend/migrations/" + migFile, - "../migrations/" + migFile, - "/home/yamamoto/lab/gohorsejobs/backend/migrations/" + migFile, + if migrationsDir == "" { + log.Fatal("Could not find migrations directory; looked in common locations") + } + + entries, err := os.ReadDir(migrationsDir) + if err != nil { + log.Fatalf("Failed reading migrations dir %s: %v", migrationsDir, err) + } + + var files []string + for _, e := range entries { + if e.IsDir() { + continue + } + name := e.Name() + if strings.HasSuffix(name, ".sql") { + files = append(files, name) + } + } + + // Sort filenames to ensure chronological order + sort.Strings(files) + + for _, migFile := range files { + fullPath := migrationsDir + "/" + migFile + log.Printf("Processing migration: %s (from %s)", migFile, fullPath) + + content, err := os.ReadFile(fullPath) + if err != nil { + log.Fatalf("Could not read migration %s: %v", fullPath, err) } - var content []byte - var readErr error - - for _, p := range paths { - content, readErr = os.ReadFile(p) - if readErr == nil { - log.Printf("Found migration at: %s", p) - break - } - } - - if content == nil { - log.Fatalf("Could not find migration file %s. Last error: %v", migFile, readErr) - } - - statements := strings.Split(string(content), ";") - for _, stmt := range statements { - trimmed := strings.TrimSpace(stmt) - if trimmed == "" { + // Execute the whole file. lib/pq supports multi-statement Exec. + sqlText := string(content) + log.Printf("Executing migration file %s", fullPath) + _, err = db.Exec(sqlText) + if err != nil { + errStr := err.Error() + // Tolerable errors: object already exists, column doesn't exist in some contexts, + // or duplicate key when updates are guarded by intent. Log and continue. + if strings.Contains(errStr, "already exists") || strings.Contains(errStr, "column") && strings.Contains(errStr, "does not exist") || strings.Contains(errStr, "duplicate key value violates unique constraint") { + log.Printf("Warning while applying %s: %v", migFile, err) continue } - log.Printf("Executing: %s", trimmed) - _, err = db.Exec(trimmed) - if err != nil { - if strings.Contains(err.Error(), "already exists") { - log.Printf("Warning (ignored): %v", err) - } else { - log.Printf("FAILED executing: %s\nError: %v", trimmed, err) - // Verify if we should stop. For now, continue best effort or fail? - // Use fatal for critical schema errors not "already exists" - log.Fatal(err) - } - } + log.Fatalf("Failed applying migration %s: %v", migFile, err) } + log.Printf("Migration %s applied successfully", migFile) } diff --git a/backend/migrations/004_create_prefectures_cities_tables.sql b/backend/migrations/004_create_prefectures_cities_tables.sql index 749a9ee..a7d322c 100755 --- a/backend/migrations/004_create_prefectures_cities_tables.sql +++ b/backend/migrations/004_create_prefectures_cities_tables.sql @@ -11,17 +11,29 @@ CREATE TABLE IF NOT EXISTS regions ( CREATE TABLE IF NOT EXISTS cities ( id SERIAL PRIMARY KEY, - region_id INT NOT NULL, + region_id INT, name VARCHAR(100) NOT NULL, - - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - - FOREIGN KEY (region_id) REFERENCES regions(id) ON DELETE CASCADE + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); --- Indexes -CREATE INDEX idx_regions_country ON regions(country_code); -CREATE INDEX idx_cities_region ON cities(region_id); +-- Ensure column and constraints exist when table already existed without them +ALTER TABLE cities + ADD COLUMN IF NOT EXISTS region_id INT; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name + WHERE tc.table_name = 'cities' AND tc.constraint_type = 'FOREIGN KEY' AND kcu.column_name = 'region_id' + ) THEN + ALTER TABLE cities ADD CONSTRAINT fk_cities_region FOREIGN KEY (region_id) REFERENCES regions(id) ON DELETE CASCADE; + END IF; +END$$; + +-- Indexes (safe if table/column already existed) +CREATE INDEX IF NOT EXISTS idx_regions_country ON regions(country_code); +CREATE INDEX IF NOT EXISTS idx_cities_region ON cities(region_id); -- Comments COMMENT ON TABLE regions IS 'Global Regions (States, Provinces, Prefectures)'; diff --git a/backend/start_dev.sh b/backend/start_dev.sh new file mode 100755 index 0000000..e75c455 --- /dev/null +++ b/backend/start_dev.sh @@ -0,0 +1,5 @@ +#!/bin/sh +export GOPATH=/go +export PATH=/go/bin:/usr/local/go/bin:$PATH +go install github.com/air-verse/air@v1.60.0 +air diff --git a/frontend/wrangler.toml b/frontend/wrangler.toml index 477b361..6c23575 100644 --- a/frontend/wrangler.toml +++ b/frontend/wrangler.toml @@ -1,5 +1,5 @@ #:schema node_modules/wrangler/config-schema.json name = "gohorsejobs-frontend" compatibility_date = "2024-09-23" -compatibility_flags = ["nodejs_als"] +compatibility_flags = ["nodejs_compat"] pages_build_output_dir = ".vercel/output/static" diff --git a/k8s/dev/backend-deployment-dev.yaml b/k8s/dev/backend-deployment-dev.yaml index 3a51196..89e3d36 100644 --- a/k8s/dev/backend-deployment-dev.yaml +++ b/k8s/dev/backend-deployment-dev.yaml @@ -21,6 +21,10 @@ spec: terminationGracePeriodSeconds: 10 imagePullSecrets: - name: forgejo-registry-secret + dnsConfig: + options: + - name: ndots + value: "1" containers: - name: backend image: pipe.gohorsejobs.com/bohessefm/gohorsejobs:latest @@ -43,7 +47,6 @@ spec: cpu: "800m" # Prioridade de CPU garantida limits: memory: "1024Mi" - cpu: "1000m" livenessProbe: httpGet: path: /health diff --git a/k8s/dev/backoffice-deployment-dev.yaml b/k8s/dev/backoffice-deployment-dev.yaml index 38365fd..6cb2593 100644 --- a/k8s/dev/backoffice-deployment-dev.yaml +++ b/k8s/dev/backoffice-deployment-dev.yaml @@ -21,6 +21,10 @@ spec: terminationGracePeriodSeconds: 10 imagePullSecrets: - name: forgejo-registry-secret + dnsConfig: + options: + - name: ndots + value: "1" containers: - name: backoffice image: pipe.gohorsejobs.com/bohessefm/backoffice:latest @@ -43,8 +47,7 @@ spec: memory: "1536Mi" cpu: "500m" limits: - memory: "2Gi" - cpu: "1000m" + memory: "2Gi" livenessProbe: httpGet: path: /health