- Docker and Docker Compose
- Go 1.26.9+
- Node.js and npm (for frontend development)
- Make (optional, for convenience commands)
# Start PostgreSQL and CUDly application
docker-compose up -d
# View logs
docker-compose logs -f app
# Stop environment
docker-compose down- CUDly API: http://localhost:8080
- PostgreSQL: localhost:5432
- Database:
cudly - User:
cudly - Password:
cudly_local_dev
- Database:
- pgAdmin (optional): http://localhost:5050
- Email:
admin@cudly.local - Password:
admin - Start with:
docker-compose --profile tools up
- Email:
Migration URLs use the
pgx5://scheme, notpostgres://. golang-migrate picks its database driver by URL scheme, and this repo buildsmigratewith-tags pgx5so the binary does not linklib/pq, which carries advisories with no published fix (issue #1849). Apostgres://URL fails against it withunknown driver postgres.
# Connect to PostgreSQL using psql
docker-compose exec postgres psql -U cudly -d cudly
# Run migrations manually
docker-compose exec app migrate -path /app/internal/database/postgres/migrations \
-database "pgx5://cudly:cudly_local_dev@postgres:5432/cudly?sslmode=disable" up
# Check migration status
docker-compose exec app migrate -path /app/internal/database/postgres/migrations \
-database "pgx5://cudly:cudly_local_dev@postgres:5432/cudly?sslmode=disable" versionThe development environment uses Air for hot reload. Any changes to .go files automatically trigger a rebuild and restart.
# Air automatically detects changes and reloads
docker-compose logs -f app# Create a new migration
migrate create -ext sql -dir internal/database/postgres/migrations -seq add_new_feature
# Run migrations
docker-compose exec app migrate -path /app/internal/database/postgres/migrations \
-database "pgx5://cudly:cudly_local_dev@postgres:5432/cudly?sslmode=disable" up
# Rollback last migration
docker-compose exec app migrate -path /app/internal/database/postgres/migrations \
-database "pgx5://cudly:cudly_local_dev@postgres:5432/cudly?sslmode=disable" down 1Configured in docker-compose.yml:
DB_HOST: PostgreSQL hostname (docker-compose:postgres)DB_PORT: PostgreSQL port (default:5432)DB_NAME: Database name (default:cudly)DB_USER: Database user (default:cudly)DB_PASSWORD: Database password (docker-compose:cudly_local_dev)DB_SSL_MODE: SSL mode (app default:require; docker-compose overrides todisable)DB_AUTO_MIGRATE: Auto-run migrations on startup (default:true)
SECRET_PROVIDER: Secret manager provider (default:env)env: Use environment variables (suitable for local dev)aws: AWS Secrets Managergcp: GCP Secret Managerazure: Azure Key Vault
CUDly encrypts stored cloud account credentials with AES-256-GCM. The encryption key must be provided via one of:
-
Local dev — set
CREDENTIAL_ENCRYPTION_KEYto a 64-character hex string (32 bytes):export CREDENTIAL_ENCRYPTION_KEY=$(openssl rand -hex 32)
Add this to your
docker-compose.ymlor.envfile for local use. -
Production (AWS Lambda) — set
CREDENTIAL_ENCRYPTION_KEY_SECRET_ARNto the ARN of an AWS Secrets Manager secret whose value is the 64-char hex key. Terraform creates this secret automatically whencreate_credential_encryption_key = trueis set insecrets.tf. See Deployment Guide andspecs/multi-account-execution/iac.mdfor details.
If neither variable is set, the application falls back to an insecure dev key and logs a warning. Never use the dev key in production.
ENVIRONMENT: Environment name (default:development)LOG_LEVEL: Logging level (default:debug)
Unit tests verify individual functions in isolation. Located in *_test.go files, run by default. Coverage target: >85%.
make test-unit
# or
go test -v -race -short ./...Integration tests verify component interactions with real dependencies (databases via testcontainers). Located in *_test.go files with //go:build integration tag. Coverage target: >80%.
make test-integration
# or
go test -v -race -tags=integration ./...E2E tests verify the complete application flow using docker-compose.
make docker-compose-test
# or
docker-compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from test-runner# Quick test (unit only)
make test
# Full test suite (unit + integration + coverage)
make full-test
# Coverage report
make test-coverage
# Generates coverage.out and coverage.html
open coverage.html
# Specific package
go test -v ./internal/server/...
# Specific function
go test -v -run TestHandleScheduledTask ./internal/server/
# Integration tests (requires PostgreSQL)
docker-compose up -d postgres
DB_HOST=localhost DB_PASSWORD=cudly_local_dev go test -tags=integration ./internal/database/...Use the AAA (Arrange-Act-Assert) pattern:
func TestMyFunction(t *testing.T) {
// Arrange
ctx := testutil.TestContext(t)
input := "test data"
// Act
result, err := MyFunction(ctx, input)
// Assert
testutil.AssertNoError(t, err)
testutil.AssertEqual(t, "expected", result)
}Table-driven tests for multiple scenarios:
func TestMultipleScenarios(t *testing.T) {
tests := []struct {
name string
input string
expected string
expectError bool
}{
{name: "valid input", input: "test", expected: "TEST"},
{name: "invalid input", input: "", expectError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Transform(tt.input)
if tt.expectError {
testutil.AssertError(t, err)
} else {
testutil.AssertNoError(t, err)
testutil.AssertEqual(t, tt.expected, result)
}
})
}
}// Context with timeout
ctx := testutil.TestContext(t)
// Environment variables
testutil.SetEnv(t, "DB_HOST", "localhost")
// Skip conditions
testutil.SkipIfShort(t)
testutil.SkipCI(t)
// Assertions
testutil.AssertNoError(t, err)
testutil.AssertEqual(t, expected, actual)
testutil.AssertTrue(t, condition, "message")
testutil.AssertContains(t, haystack, needle)
// Wait for condition
testutil.WaitFor(t, func() bool {
return server.IsReady()
}, 5*time.Second, "server to be ready")mockScheduler := &testutil.MockScheduler{
CollectRecommendationsFunc: func(ctx context.Context) (*scheduler.CollectResult, error) {
return &scheduler.CollectResult{Count: 10}, nil
},
}
app := &Application{Scheduler: mockScheduler}//go:build integration
func TestWithPostgres(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
ctx := testutil.TestContext(t)
pg, err := testutil.SetupPostgresContainer(ctx, t)
testutil.AssertNoError(t, err)
for k, v := range pg.Config() {
testutil.SetEnv(t, k, v)
}
// Test with real database...
}Targets: Unit >85%, Integration >80%, Critical paths 100%
make test-coverage
go tool cover -func=coverage.out # by package
go tool cover -html=coverage.out # in browserExceptions: Generated code, trivial getters/setters, unreachable panic handlers.
Tests run on PRs, pushes to main, and release tags via .github/workflows/ci.yml.
# Run full CI pipeline locally
make ci # formatting, vet, complexity check, unit tests, security scanning, terraform validation
# Pre-commit hook
make pre-commit # formatting, vet, complexity check, unit tests# All security scans
make security-scan
# Individual scans
make security-scan-go # gosec
make security-scan-docker # trivy (container + filesystem)
make security-scan-terraform # tfsecmake terraform-validate
make terraform-fmt-check
make terraform-fmt
make docker-build # build Docker image
make docker-test # build and test image
make docker-compose-test # E2E tests with docker-composeThe frontend is a TypeScript application in frontend/src/, built with webpack.
cd frontend
# Install dependencies
npm install
# Development build (with watch)
npm run dev
# Production build
npm run build
# Run tests
npx jest
# Run tests with coverage
npx jest --coverageThe frontend builds to frontend/dist/ and is deployed to CDN (CloudFront/Azure CDN/Cloud CDN) as static files.
docker-compose ps postgres
docker-compose logs postgres
docker-compose exec app pg_isready -h postgres -U cudly# Check current migration version
docker-compose exec postgres psql -U cudly -d cudly -c "SELECT * FROM schema_migrations;"
# Force migration version (use with caution)
migrate -path internal/database/postgres/migrations \
-database "pgx5://cudly:cudly_local_dev@localhost:5432/cudly?sslmode=disable" \
force <version>docker-compose down -v
docker-compose up --build -d- "context deadline exceeded": Increase timeout in
testutil.TestContext()or use a longer context - Docker not available: Integration tests require Docker: https://docs.docker.com/get-docker/
- testcontainers fails: Ensure Docker daemon is running (
docker ps) - Race condition detected: Run with
go test -race ./... - Coverage too low: Find uncovered code with
go tool cover -func=coverage.out | grep -v "100.0%"
# Option 1: Mount AWS credentials
# In docker-compose.yml:
# app:
# environment:
# SECRET_PROVIDER: aws
# volumes:
# - ~/.aws:/root/.aws:ro
# Option 2: Environment variables
export AWS_ACCESS_KEY_ID=xxx
export AWS_SECRET_ACCESS_KEY=xxx
export AWS_REGION=us-east-1
docker-compose restart app# Build production image
docker build -t cudly:latest .
# Run with PostgreSQL
docker run --rm \
--network cudly_cudly-network \
-e DB_HOST=postgres \
-e DB_PASSWORD=cudly_local_dev \
-e RUNTIME_MODE=http \
-p 8080:8080 \
cudly:latest