Implements the full host ↔ control plane connection flow:
- Host CRUD endpoints (POST/GET/DELETE /v1/hosts) with role-based access:
regular hosts admin-only, BYOC hosts for admins and team owners
- One-time registration token flow: admin creates host → gets token (1hr TTL
in Redis + Postgres audit trail) → host agent registers with specs → gets
long-lived JWT (1yr)
- Host agent registration client with automatic spec detection (arch, CPU,
memory, disk) and token persistence to disk
- Periodic heartbeat (30s) via POST /v1/hosts/{id}/heartbeat with X-Host-Token
auth and host ID cross-check
- Token regeneration endpoint (POST /v1/hosts/{id}/token) for retry after
failed registration
- Tag management (add/remove/list) with team-scoped access control
- Host JWT with typ:"host" claim, cross-use prevention in both VerifyJWT and
VerifyHostJWT
- requireHostToken middleware for host agent authentication
- DB-level race protection: RegisterHost uses AND status='pending' with
rows-affected check; Redis GetDel for atomic token consume
- Migration for future mTLS support (cert_fingerprint, mtls_enabled columns)
- Host agent flags: --register (one-time token), --address (required ip:port)
- serviceErrToHTTP extended with "forbidden" → 403 mapping
- OpenAPI spec, .env.example, and README updated
537 lines
13 KiB
Go
537 lines
13 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.30.0
|
|
// source: hosts.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const addHostTag = `-- name: AddHostTag :exec
|
|
INSERT INTO host_tags (host_id, tag) VALUES ($1, $2) ON CONFLICT DO NOTHING
|
|
`
|
|
|
|
type AddHostTagParams struct {
|
|
HostID string `json:"host_id"`
|
|
Tag string `json:"tag"`
|
|
}
|
|
|
|
func (q *Queries) AddHostTag(ctx context.Context, arg AddHostTagParams) error {
|
|
_, err := q.db.Exec(ctx, addHostTag, arg.HostID, arg.Tag)
|
|
return err
|
|
}
|
|
|
|
const deleteHost = `-- name: DeleteHost :exec
|
|
DELETE FROM hosts WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteHost(ctx context.Context, id string) error {
|
|
_, err := q.db.Exec(ctx, deleteHost, id)
|
|
return err
|
|
}
|
|
|
|
const getHost = `-- name: GetHost :one
|
|
SELECT id, type, team_id, provider, availability_zone, arch, cpu_cores, memory_mb, disk_gb, address, status, last_heartbeat_at, metadata, created_by, created_at, updated_at, cert_fingerprint, mtls_enabled FROM hosts WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetHost(ctx context.Context, id string) (Host, error) {
|
|
row := q.db.QueryRow(ctx, getHost, id)
|
|
var i Host
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Type,
|
|
&i.TeamID,
|
|
&i.Provider,
|
|
&i.AvailabilityZone,
|
|
&i.Arch,
|
|
&i.CpuCores,
|
|
&i.MemoryMb,
|
|
&i.DiskGb,
|
|
&i.Address,
|
|
&i.Status,
|
|
&i.LastHeartbeatAt,
|
|
&i.Metadata,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CertFingerprint,
|
|
&i.MtlsEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getHostByTeam = `-- name: GetHostByTeam :one
|
|
SELECT id, type, team_id, provider, availability_zone, arch, cpu_cores, memory_mb, disk_gb, address, status, last_heartbeat_at, metadata, created_by, created_at, updated_at, cert_fingerprint, mtls_enabled FROM hosts WHERE id = $1 AND team_id = $2
|
|
`
|
|
|
|
type GetHostByTeamParams struct {
|
|
ID string `json:"id"`
|
|
TeamID pgtype.Text `json:"team_id"`
|
|
}
|
|
|
|
func (q *Queries) GetHostByTeam(ctx context.Context, arg GetHostByTeamParams) (Host, error) {
|
|
row := q.db.QueryRow(ctx, getHostByTeam, arg.ID, arg.TeamID)
|
|
var i Host
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Type,
|
|
&i.TeamID,
|
|
&i.Provider,
|
|
&i.AvailabilityZone,
|
|
&i.Arch,
|
|
&i.CpuCores,
|
|
&i.MemoryMb,
|
|
&i.DiskGb,
|
|
&i.Address,
|
|
&i.Status,
|
|
&i.LastHeartbeatAt,
|
|
&i.Metadata,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CertFingerprint,
|
|
&i.MtlsEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getHostTags = `-- name: GetHostTags :many
|
|
SELECT tag FROM host_tags WHERE host_id = $1 ORDER BY tag
|
|
`
|
|
|
|
func (q *Queries) GetHostTags(ctx context.Context, hostID string) ([]string, error) {
|
|
rows, err := q.db.Query(ctx, getHostTags, hostID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []string
|
|
for rows.Next() {
|
|
var tag string
|
|
if err := rows.Scan(&tag); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, tag)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getHostTokensByHost = `-- name: GetHostTokensByHost :many
|
|
SELECT id, host_id, created_by, created_at, expires_at, used_at FROM host_tokens WHERE host_id = $1 ORDER BY created_at DESC
|
|
`
|
|
|
|
func (q *Queries) GetHostTokensByHost(ctx context.Context, hostID string) ([]HostToken, error) {
|
|
rows, err := q.db.Query(ctx, getHostTokensByHost, hostID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []HostToken
|
|
for rows.Next() {
|
|
var i HostToken
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.HostID,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.ExpiresAt,
|
|
&i.UsedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const insertHost = `-- name: InsertHost :one
|
|
INSERT INTO hosts (id, type, team_id, provider, availability_zone, created_by)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING id, type, team_id, provider, availability_zone, arch, cpu_cores, memory_mb, disk_gb, address, status, last_heartbeat_at, metadata, created_by, created_at, updated_at, cert_fingerprint, mtls_enabled
|
|
`
|
|
|
|
type InsertHostParams struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
TeamID pgtype.Text `json:"team_id"`
|
|
Provider pgtype.Text `json:"provider"`
|
|
AvailabilityZone pgtype.Text `json:"availability_zone"`
|
|
CreatedBy string `json:"created_by"`
|
|
}
|
|
|
|
func (q *Queries) InsertHost(ctx context.Context, arg InsertHostParams) (Host, error) {
|
|
row := q.db.QueryRow(ctx, insertHost,
|
|
arg.ID,
|
|
arg.Type,
|
|
arg.TeamID,
|
|
arg.Provider,
|
|
arg.AvailabilityZone,
|
|
arg.CreatedBy,
|
|
)
|
|
var i Host
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Type,
|
|
&i.TeamID,
|
|
&i.Provider,
|
|
&i.AvailabilityZone,
|
|
&i.Arch,
|
|
&i.CpuCores,
|
|
&i.MemoryMb,
|
|
&i.DiskGb,
|
|
&i.Address,
|
|
&i.Status,
|
|
&i.LastHeartbeatAt,
|
|
&i.Metadata,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CertFingerprint,
|
|
&i.MtlsEnabled,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const insertHostToken = `-- name: InsertHostToken :one
|
|
INSERT INTO host_tokens (id, host_id, created_by, expires_at)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id, host_id, created_by, created_at, expires_at, used_at
|
|
`
|
|
|
|
type InsertHostTokenParams struct {
|
|
ID string `json:"id"`
|
|
HostID string `json:"host_id"`
|
|
CreatedBy string `json:"created_by"`
|
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
|
}
|
|
|
|
func (q *Queries) InsertHostToken(ctx context.Context, arg InsertHostTokenParams) (HostToken, error) {
|
|
row := q.db.QueryRow(ctx, insertHostToken,
|
|
arg.ID,
|
|
arg.HostID,
|
|
arg.CreatedBy,
|
|
arg.ExpiresAt,
|
|
)
|
|
var i HostToken
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.HostID,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.ExpiresAt,
|
|
&i.UsedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listHosts = `-- name: ListHosts :many
|
|
SELECT id, type, team_id, provider, availability_zone, arch, cpu_cores, memory_mb, disk_gb, address, status, last_heartbeat_at, metadata, created_by, created_at, updated_at, cert_fingerprint, mtls_enabled FROM hosts ORDER BY created_at DESC
|
|
`
|
|
|
|
func (q *Queries) ListHosts(ctx context.Context) ([]Host, error) {
|
|
rows, err := q.db.Query(ctx, listHosts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Host
|
|
for rows.Next() {
|
|
var i Host
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Type,
|
|
&i.TeamID,
|
|
&i.Provider,
|
|
&i.AvailabilityZone,
|
|
&i.Arch,
|
|
&i.CpuCores,
|
|
&i.MemoryMb,
|
|
&i.DiskGb,
|
|
&i.Address,
|
|
&i.Status,
|
|
&i.LastHeartbeatAt,
|
|
&i.Metadata,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CertFingerprint,
|
|
&i.MtlsEnabled,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listHostsByStatus = `-- name: ListHostsByStatus :many
|
|
SELECT id, type, team_id, provider, availability_zone, arch, cpu_cores, memory_mb, disk_gb, address, status, last_heartbeat_at, metadata, created_by, created_at, updated_at, cert_fingerprint, mtls_enabled FROM hosts WHERE status = $1 ORDER BY created_at DESC
|
|
`
|
|
|
|
func (q *Queries) ListHostsByStatus(ctx context.Context, status string) ([]Host, error) {
|
|
rows, err := q.db.Query(ctx, listHostsByStatus, status)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Host
|
|
for rows.Next() {
|
|
var i Host
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Type,
|
|
&i.TeamID,
|
|
&i.Provider,
|
|
&i.AvailabilityZone,
|
|
&i.Arch,
|
|
&i.CpuCores,
|
|
&i.MemoryMb,
|
|
&i.DiskGb,
|
|
&i.Address,
|
|
&i.Status,
|
|
&i.LastHeartbeatAt,
|
|
&i.Metadata,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CertFingerprint,
|
|
&i.MtlsEnabled,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listHostsByTag = `-- name: ListHostsByTag :many
|
|
SELECT h.id, h.type, h.team_id, h.provider, h.availability_zone, h.arch, h.cpu_cores, h.memory_mb, h.disk_gb, h.address, h.status, h.last_heartbeat_at, h.metadata, h.created_by, h.created_at, h.updated_at, h.cert_fingerprint, h.mtls_enabled FROM hosts h
|
|
JOIN host_tags ht ON ht.host_id = h.id
|
|
WHERE ht.tag = $1
|
|
ORDER BY h.created_at DESC
|
|
`
|
|
|
|
func (q *Queries) ListHostsByTag(ctx context.Context, tag string) ([]Host, error) {
|
|
rows, err := q.db.Query(ctx, listHostsByTag, tag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Host
|
|
for rows.Next() {
|
|
var i Host
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Type,
|
|
&i.TeamID,
|
|
&i.Provider,
|
|
&i.AvailabilityZone,
|
|
&i.Arch,
|
|
&i.CpuCores,
|
|
&i.MemoryMb,
|
|
&i.DiskGb,
|
|
&i.Address,
|
|
&i.Status,
|
|
&i.LastHeartbeatAt,
|
|
&i.Metadata,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CertFingerprint,
|
|
&i.MtlsEnabled,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listHostsByTeam = `-- name: ListHostsByTeam :many
|
|
SELECT id, type, team_id, provider, availability_zone, arch, cpu_cores, memory_mb, disk_gb, address, status, last_heartbeat_at, metadata, created_by, created_at, updated_at, cert_fingerprint, mtls_enabled FROM hosts WHERE team_id = $1 AND type = 'byoc' ORDER BY created_at DESC
|
|
`
|
|
|
|
func (q *Queries) ListHostsByTeam(ctx context.Context, teamID pgtype.Text) ([]Host, error) {
|
|
rows, err := q.db.Query(ctx, listHostsByTeam, teamID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Host
|
|
for rows.Next() {
|
|
var i Host
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Type,
|
|
&i.TeamID,
|
|
&i.Provider,
|
|
&i.AvailabilityZone,
|
|
&i.Arch,
|
|
&i.CpuCores,
|
|
&i.MemoryMb,
|
|
&i.DiskGb,
|
|
&i.Address,
|
|
&i.Status,
|
|
&i.LastHeartbeatAt,
|
|
&i.Metadata,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CertFingerprint,
|
|
&i.MtlsEnabled,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listHostsByType = `-- name: ListHostsByType :many
|
|
SELECT id, type, team_id, provider, availability_zone, arch, cpu_cores, memory_mb, disk_gb, address, status, last_heartbeat_at, metadata, created_by, created_at, updated_at, cert_fingerprint, mtls_enabled FROM hosts WHERE type = $1 ORDER BY created_at DESC
|
|
`
|
|
|
|
func (q *Queries) ListHostsByType(ctx context.Context, type_ string) ([]Host, error) {
|
|
rows, err := q.db.Query(ctx, listHostsByType, type_)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Host
|
|
for rows.Next() {
|
|
var i Host
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Type,
|
|
&i.TeamID,
|
|
&i.Provider,
|
|
&i.AvailabilityZone,
|
|
&i.Arch,
|
|
&i.CpuCores,
|
|
&i.MemoryMb,
|
|
&i.DiskGb,
|
|
&i.Address,
|
|
&i.Status,
|
|
&i.LastHeartbeatAt,
|
|
&i.Metadata,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.CertFingerprint,
|
|
&i.MtlsEnabled,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const markHostTokenUsed = `-- name: MarkHostTokenUsed :exec
|
|
UPDATE host_tokens SET used_at = NOW() WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) MarkHostTokenUsed(ctx context.Context, id string) error {
|
|
_, err := q.db.Exec(ctx, markHostTokenUsed, id)
|
|
return err
|
|
}
|
|
|
|
const registerHost = `-- name: RegisterHost :execrows
|
|
UPDATE hosts
|
|
SET arch = $2,
|
|
cpu_cores = $3,
|
|
memory_mb = $4,
|
|
disk_gb = $5,
|
|
address = $6,
|
|
status = 'online',
|
|
last_heartbeat_at = NOW(),
|
|
updated_at = NOW()
|
|
WHERE id = $1 AND status = 'pending'
|
|
`
|
|
|
|
type RegisterHostParams struct {
|
|
ID string `json:"id"`
|
|
Arch pgtype.Text `json:"arch"`
|
|
CpuCores pgtype.Int4 `json:"cpu_cores"`
|
|
MemoryMb pgtype.Int4 `json:"memory_mb"`
|
|
DiskGb pgtype.Int4 `json:"disk_gb"`
|
|
Address pgtype.Text `json:"address"`
|
|
}
|
|
|
|
func (q *Queries) RegisterHost(ctx context.Context, arg RegisterHostParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, registerHost,
|
|
arg.ID,
|
|
arg.Arch,
|
|
arg.CpuCores,
|
|
arg.MemoryMb,
|
|
arg.DiskGb,
|
|
arg.Address,
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const removeHostTag = `-- name: RemoveHostTag :exec
|
|
DELETE FROM host_tags WHERE host_id = $1 AND tag = $2
|
|
`
|
|
|
|
type RemoveHostTagParams struct {
|
|
HostID string `json:"host_id"`
|
|
Tag string `json:"tag"`
|
|
}
|
|
|
|
func (q *Queries) RemoveHostTag(ctx context.Context, arg RemoveHostTagParams) error {
|
|
_, err := q.db.Exec(ctx, removeHostTag, arg.HostID, arg.Tag)
|
|
return err
|
|
}
|
|
|
|
const updateHostHeartbeat = `-- name: UpdateHostHeartbeat :exec
|
|
UPDATE hosts SET last_heartbeat_at = NOW(), updated_at = NOW() WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) UpdateHostHeartbeat(ctx context.Context, id string) error {
|
|
_, err := q.db.Exec(ctx, updateHostHeartbeat, id)
|
|
return err
|
|
}
|
|
|
|
const updateHostStatus = `-- name: UpdateHostStatus :exec
|
|
UPDATE hosts SET status = $2, updated_at = NOW() WHERE id = $1
|
|
`
|
|
|
|
type UpdateHostStatusParams struct {
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func (q *Queries) UpdateHostStatus(ctx context.Context, arg UpdateHostStatusParams) error {
|
|
_, err := q.db.Exec(ctx, updateHostStatus, arg.ID, arg.Status)
|
|
return err
|
|
}
|