Files
headscale/hscontrol/db/policy.go
T
Kristoffer Dalby 524ccc46aa db: keep only the current policy
Every write inserted a row and nothing deleted one, while every reader
takes only the newest. Writers store and install under one lock, so an
uncommitted insert cannot outlive the newer row that pruned the rest.
2026-10-28 14:37:08 +00:00

102 lines
2.3 KiB
Go

package db
import (
"errors"
"os"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// SetPolicy replaces the policy in the database. Only the newest row is
// ever read, so older ones are removed rather than kept as history.
func (hsdb *HSDatabase) SetPolicy(policy string) (*types.Policy, error) {
return Write(hsdb.DB, func(tx *gorm.DB) (*types.Policy, error) {
p := types.Policy{
Data: policy,
}
err := tx.Clauses(clause.Returning{}).Create(&p).Error
if err != nil {
return nil, err
}
// Unscoped: a soft delete would keep the data. Deleting by id
// never removes a newer row from a concurrent writer.
err = tx.Unscoped().Where("id < ?", p.ID).Delete(&types.Policy{}).Error
if err != nil {
return nil, err
}
return &p, nil
})
}
// GetPolicy returns the latest policy in the database.
func (hsdb *HSDatabase) GetPolicy() (*types.Policy, error) {
return GetPolicy(hsdb.DB)
}
// GetPolicy returns the latest policy from the database.
// This standalone function can be used in contexts where [HSDatabase] is not available,
// such as during migrations.
func GetPolicy(tx *gorm.DB) (*types.Policy, error) {
var p types.Policy
// Query:
// SELECT * FROM policies ORDER BY id DESC LIMIT 1;
err := tx.
Order("id DESC").
Limit(1).
First(&p).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, types.ErrPolicyNotFound
}
return nil, err
}
return &p, nil
}
// PolicyBytes loads policy configuration from file or database based on the configured mode.
// Returns nil if no policy is configured, which is valid.
// This standalone function can be used in contexts where [HSDatabase] is not available,
// such as during migrations.
func PolicyBytes(tx *gorm.DB, cfg *types.Config) ([]byte, error) {
switch cfg.Policy.Mode {
case types.PolicyModeFile:
path := cfg.Policy.Path
// It is fine to start headscale without a policy file.
if len(path) == 0 {
return nil, nil
}
absPath := util.AbsolutePathFromConfigPath(path)
return os.ReadFile(absPath)
case types.PolicyModeDB:
p, err := GetPolicy(tx)
if err != nil {
if errors.Is(err, types.ErrPolicyNotFound) {
return nil, nil
}
return nil, err
}
if p.Data == "" {
return nil, nil
}
return []byte(p.Data), nil
}
return nil, nil
}