diff --git a/.docker/Dockerfile.backend.dev b/.docker/Dockerfile.backend.dev new file mode 100644 index 00000000..0567fa96 --- /dev/null +++ b/.docker/Dockerfile.backend.dev @@ -0,0 +1,5 @@ +FROM golang:1.25-alpine + +RUN go install github.com/air-verse/air@latest + +WORKDIR /app diff --git a/.docker/Dockerfile.frontend.dev b/.docker/Dockerfile.frontend.dev new file mode 100644 index 00000000..5100dc65 --- /dev/null +++ b/.docker/Dockerfile.frontend.dev @@ -0,0 +1,9 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/.docker/docker-compose.dev.yml b/.docker/docker-compose.dev.yml new file mode 100644 index 00000000..9913a4f1 --- /dev/null +++ b/.docker/docker-compose.dev.yml @@ -0,0 +1,69 @@ +services: + frontend: + build: + context: .. + dockerfile: .docker/Dockerfile.frontend.dev + ports: + - "5173:5173" + environment: + - NODE_ENV=development + - BACKEND_URL=http://backend:3001 + volumes: + - ..:/app + - /app/node_modules + depends_on: + backend: + condition: service_healthy + + backend: + build: + context: .. + dockerfile: .docker/Dockerfile.backend.dev + working_dir: /app + command: air + ports: + - "3001:3001" + environment: + - DB_HOST=postgres + - DB_PORT=5432 + - DB_NAME=crossview + - DB_USER=postgres + - DB_PASSWORD=password + - SESSION_SECRET=dev-secret-change-in-production + - CORS_ORIGIN=http://localhost:5173 + - KUBECONFIG=/root/.kube/config + - KUBE_SERVER=${KUBE_SERVER:-} + - KUBE_INSECURE_SKIP_TLS_VERIFY=${KUBE_INSECURE_SKIP_TLS_VERIFY:-} + volumes: + - ../crossview-go-server:/app + - go-cache:/root/go/pkg/mod + - ~/.kube/config:/root/.kube/config:ro + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/health"] + interval: 5s + timeout: 5s + retries: 12 + start_period: 15s + depends_on: + postgres: + condition: service_healthy + + postgres: + image: postgres:17 + environment: + - POSTGRES_DB=crossview + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + ports: + - "5432:5432" + volumes: + - postgres-dev-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + go-cache: + postgres-dev-data: diff --git a/.gitignore b/.gitignore index 69666377..4ccdf421 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ dist-ssr # Configuration files (contain sensitive info) config/database.json config/config.yaml +.env diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..fabcfa1f --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +.PHONY: help dev dev-down + +ENV_FILE := $(wildcard .env) +COMPOSE := docker compose $(if $(ENV_FILE),--env-file .env,) -f .docker/docker-compose.dev.yml + +help: ## Show available commands + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +dev: ## Start dev environment in detached mode (frontend + backend + postgres) + $(COMPOSE) up --build -d + +dev-down: ## Stop dev environment + $(COMPOSE) down diff --git a/crossview-go-server/.air.toml b/crossview-go-server/.air.toml new file mode 100644 index 00000000..6aa2bad2 --- /dev/null +++ b/crossview-go-server/.air.toml @@ -0,0 +1,46 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = ["app:serve"] + bin = "./tmp/server" + cmd = "go build -o ./tmp/server ./main.go" + delay = 1000 + exclude_dir = ["vendor", "tmp", "testdata"] + exclude_file = [] + exclude_regex = ["_test\\.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/crossview-go-server/.gitignore b/crossview-go-server/.gitignore new file mode 100644 index 00000000..3fec32c8 --- /dev/null +++ b/crossview-go-server/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/crossview-go-server/lib/env.go b/crossview-go-server/lib/env.go index 6f55519a..e724b291 100644 --- a/crossview-go-server/lib/env.go +++ b/crossview-go-server/lib/env.go @@ -9,21 +9,23 @@ import ( ) type Env struct { - ServerPort string `mapstructure:"SERVER_PORT"` - Environment string `mapstructure:"ENV"` - LogOutput string `mapstructure:"LOG_OUTPUT"` - LogLevel string `mapstructure:"LOG_LEVEL"` - DBUsername string `mapstructure:"DB_USER"` - DBPassword string `mapstructure:"DB_PASS"` - DBHost string `mapstructure:"DB_HOST"` - DBPort string `mapstructure:"DB_PORT"` - DBName string `mapstructure:"DB_NAME"` - SessionSecret string `mapstructure:"SESSION_SECRET"` - CORSOrigin string `mapstructure:"CORS_ORIGIN"` - AuthMode string `mapstructure:"AUTH_MODE"` - AuthTrustedHeader string `mapstructure:"AUTH_TRUSTED_HEADER"` - AuthCreateUsers bool `mapstructure:"AUTH_CREATE_USERS"` - AuthDefaultRole string `mapstructure:"AUTH_DEFAULT_ROLE"` + ServerPort string `mapstructure:"SERVER_PORT"` + Environment string `mapstructure:"ENV"` + LogOutput string `mapstructure:"LOG_OUTPUT"` + LogLevel string `mapstructure:"LOG_LEVEL"` + DBUsername string `mapstructure:"DB_USER"` + DBPassword string `mapstructure:"DB_PASS"` + DBHost string `mapstructure:"DB_HOST"` + DBPort string `mapstructure:"DB_PORT"` + DBName string `mapstructure:"DB_NAME"` + SessionSecret string `mapstructure:"SESSION_SECRET"` + CORSOrigin string `mapstructure:"CORS_ORIGIN"` + AuthMode string `mapstructure:"AUTH_MODE"` + AuthTrustedHeader string `mapstructure:"AUTH_TRUSTED_HEADER"` + AuthCreateUsers bool `mapstructure:"AUTH_CREATE_USERS"` + AuthDefaultRole string `mapstructure:"AUTH_DEFAULT_ROLE"` + KubeServer string `mapstructure:"KUBE_SERVER"` + KubeInsecureSkipTLSVerify bool `mapstructure:"KUBE_INSECURE_SKIP_TLS_VERIFY"` } func NewEnv() Env { @@ -89,6 +91,13 @@ func NewEnv() Env { getConfigValue("server.auth.header.trustedHeader", viper.GetString("AUTH_TRUSTED_HEADER"), "X-Auth-User"))) env.AuthDefaultRole = getEnvOrDefault("AUTH_DEFAULT_ROLE", getConfigValue("server.auth.header.defaultRole", viper.GetString("AUTH_DEFAULT_ROLE"), "viewer")) + env.KubeServer = getEnvOrDefault("KUBE_SERVER", + getConfigValue("kube.server", viper.GetString("KUBE_SERVER"), "")) + if v := os.Getenv("KUBE_INSECURE_SKIP_TLS_VERIFY"); v != "" { + env.KubeInsecureSkipTLSVerify = v == "true" || v == "1" + } else if viper.IsSet("kube.insecureSkipTLSVerify") { + env.KubeInsecureSkipTLSVerify = viper.GetBool("kube.insecureSkipTLSVerify") + } if v := os.Getenv("AUTH_CREATE_USERS"); v != "" { env.AuthCreateUsers = v == "true" || v == "1" } else if viper.IsSet("server.auth.header.createUsers") { diff --git a/crossview-go-server/services/kubernetes_context.go b/crossview-go-server/services/kubernetes_context.go index 461cb3cc..311db428 100644 --- a/crossview-go-server/services/kubernetes_context.go +++ b/crossview-go-server/services/kubernetes_context.go @@ -101,7 +101,12 @@ func (k *KubernetesService) SetContext(ctxName string) error { k.kubeConfig.CurrentContext = ctxName k.currentContext = ctxName - restConfig, err = clientcmd.NewDefaultClientConfig(*k.kubeConfig, &clientcmd.ConfigOverrides{}).ClientConfig() + overrides := &clientcmd.ConfigOverrides{} + if k.env.KubeServer != "" { + overrides.ClusterInfo.Server = k.env.KubeServer + } + overrides.ClusterInfo.InsecureSkipTLSVerify = k.env.KubeInsecureSkipTLSVerify + restConfig, err = clientcmd.NewDefaultClientConfig(*k.kubeConfig, overrides).ClientConfig() if err != nil { k.failedContexts[targetContext] = true return fmt.Errorf("failed to create rest config: %w", err) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 006e1518..d7e65931 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -22,8 +22,46 @@ Thank you for your interest in contributing to Crossview. This guide will help y ### Development Setup +#### Option 1 — Docker (recommended) + +The fastest way to get a full local environment (frontend + backend + PostgreSQL) running with hot reload is via Docker Compose. Make sure you have **Docker** and **Docker Compose** installed. + +```bash +make dev # build and start all services in detached mode +make dev-down # stop and remove the containers +``` + +Services started: + +| Service | URL / Port | Notes | +| ---------- | --------------------- | -------------------------------------------------------- | +| Frontend | http://localhost:5173 | Vite dev server with HMR | +| Backend | http://localhost:3001 | Go + [Air](https://github.com/air-verse/air) live reload | +| PostgreSQL | localhost:5432 | DB: `crossview`, user: `postgres` | + +The backend uses [Air](https://github.com/air-verse/air) (configured in `crossview-go-server/.air.toml`) and rebuilds automatically on every `.go` file change. The frontend uses Vite's built-in HMR. Both source trees are mounted as volumes, so no rebuild of the Docker image is needed during development. + +> **Kubeconfig:** Your host `~/.kube/config` is mounted read-only into the backend container at `/root/.kube/config`. Make sure this file exists and contains a valid context before running `make dev`. +> +> **kind clusters:** The Kubernetes API server address in your kubeconfig (e.g. `https://127.0.0.1:`) resolves to the container itself, not your host. Create a `.env` file at the project root (it is gitignored) with the following variables: +> +> ```dotenv +> # Replace with the port shown in your kubeconfig for the kind cluster. +> # host.docker.internal resolves to the host machine on macOS and Windows. +> # On Linux, use the Docker bridge IP (172.17.0.1) or add "127.0.0.1 host.docker.internal" to /etc/hosts. +> KUBE_SERVER=https://host.docker.internal: +> +> # kind uses a self-signed certificate that does not include host.docker.internal as a SAN, +> # so TLS verification must be disabled when overriding the server address. +> KUBE_INSECURE_SKIP_TLS_VERIFY=true +> ``` +> +> The `.env` file is automatically picked up by `make dev` and passed to Docker Compose via `--env-file`. + +#### Option 2 — Manual + - **Frontend:** Node.js 20+, `npm install`, `npm run dev` -- **Backend:** Go 1.24+, `cd crossview-go-server && go run main.go app:serve` +- **Backend:** Go 1.25+, `cd crossview-go-server && go run main.go app:serve` - **Config:** Copy `config/examples/config.yaml.example` to `config/config.yaml` and adjust as needed. See [Getting Started](GETTING_STARTED.md) and [Configuration](CONFIGURATION.md) for full details. diff --git a/vite.config.js b/vite.config.js index 09898d64..45c0bfbf 100644 --- a/vite.config.js +++ b/vite.config.js @@ -27,7 +27,7 @@ export default defineConfig({ server: { proxy: { '/api': { - target: viteConfig?.server?.proxy?.api?.target || 'http://localhost:3001', + target: process.env.BACKEND_URL || viteConfig?.server?.proxy?.api?.target || 'http://localhost:3001', changeOrigin: viteConfig?.server?.proxy?.api?.changeOrigin !== false, }, },