Files
housecarl/internal/server/ops.go
T
abrown38andClaude Opus 5.5 05c811bddc Rules: placed in the same change they are created in
hc rule add --before/--after/--first created the rule at the end of its
chain and then moved it: two history entries, two probations, the rule
briefly in force in the wrong place, and in a changeset the place was
lost. POST /rules (and the other ordered kinds) now takes ?before=ID,
?after=ID or ?first=1, kept with the operation when staged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:18:37 +01:00

293 lines
7.8 KiB
Go

package server
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"housecarl/internal/api"
"housecarl/internal/engine"
"housecarl/internal/model"
)
// Every configuration change is an operation (model.Op): applied at once,
// or staged in a changeset and replayed when the changeset is applied.
// applyOp is the one place an operation becomes an edit.
// applyOp makes an operation's change to st. It returns the ID of the
// object it made or changed (0 for deletes and settings).
func applyOp(st *model.State, op *model.Op) (int64, error) {
switch op.Op {
case model.OpCreate:
return createOp(st, op)
case model.OpUpdate:
return updateOp(st, op)
case model.OpDelete:
return 0, deleteOp(st, op)
case model.OpMove:
return moveOp(st, op)
case model.OpSettings:
return 0, settingsOp(st, op)
case model.OpRevert:
var h model.HistoryEntry
if err := json.Unmarshal(op.Data, &h); err != nil {
return 0, badRequest("revert: %v", err)
}
return 0, engine.Undo(st, &h)
}
return 0, badRequest("unknown operation %q", op.Op)
}
func createOp(st *model.State, op *model.Op) (int64, error) {
o, err := decodeObject(op.Kind, op.Data)
if err != nil {
return 0, err
}
if svc, ok := o.(*model.Service); ok {
svc.Builtin = false
}
if rule, ok := o.(*model.Rule); ok && rule.Action == "" {
rule.Action = model.ActionAccept
}
if f, ok := o.(*model.PortForward); ok && len(f.Protocols) == 0 {
f.Protocols = []string{"tcp"}
}
if st.ByID(op.Kind, op.ID) != nil {
return 0, conflict("%s %d exists already", op.Kind, op.ID)
}
setID(o, op.ID)
if pos := position(o); pos != nil && *pos == 0 {
*pos = lastPosition(st, op.Kind, chainOf(o))
}
data, _ := json.Marshal(o)
if err := st.Put(op.Kind, op.ID, data); err != nil {
return 0, err
}
// Placed in the same change, so it is never in force anywhere else.
if len(op.Place) > 0 {
if _, err := moveOp(st, &model.Op{Op: model.OpMove, Kind: op.Kind, Ref: strconv.FormatInt(op.ID, 10), Data: op.Place}); err != nil {
return 0, err
}
}
return op.ID, nil
}
func updateOp(st *model.State, op *model.Op) (int64, error) {
var patch map[string]json.RawMessage
if err := json.Unmarshal(op.Data, &patch); err != nil {
return 0, badRequest("%s: %v", op.Kind, err)
}
delete(patch, "id")
o, err := findObject(st, op.Kind, op.Ref)
if err != nil {
return 0, err
}
id := o.GetID()
// The patch's fields over the object's.
var cur map[string]json.RawMessage
b, _ := json.Marshal(o)
json.Unmarshal(b, &cur)
for k, v := range patch {
if string(v) == "null" {
delete(cur, k)
} else {
cur[k] = v
}
}
merged, _ := json.Marshal(cur)
next, err := decodeObject(op.Kind, merged)
if err != nil {
return 0, err
}
setID(next, id)
if old, now := o.Label(), next.Label(); old != now && isNamed(op.Kind) {
if set, ok := o.(*model.AddressSet); ok && set.Dynamic {
return 0, conflict("set %s is dynamic: its entries, and the API tokens and programs that fill it, go by its name; make a new set instead", old)
}
renameRefs(st, op.Kind, old, now)
}
data, _ := json.Marshal(next)
return id, st.Put(op.Kind, id, data)
}
// isNamed reports whether other objects refer to a kind by name.
func isNamed(kind string) bool {
return kind == model.KindZone || kind == model.KindSet || kind == model.KindService
}
func deleteOp(st *model.State, op *model.Op) error {
o, err := findObject(st, op.Kind, op.Ref)
if err != nil {
return err
}
if used := usedBy(st, op.Kind, o); used != "" {
return conflict("%s %s is used by %s; change that first", op.Kind, o.Label(), used)
}
return st.Put(op.Kind, o.GetID(), nil)
}
// moveOp reorders a rule or NAT rule: before or after another, or to the
// start or end; a rule can change chain at the same time.
func moveOp(st *model.State, op *model.Op) (int64, error) {
var in api.MoveInput
if err := json.Unmarshal(op.Data, &in); err != nil {
return 0, badRequest("move: %v", err)
}
o, err := findObject(st, op.Kind, op.Ref)
if err != nil {
return 0, err
}
id := o.GetID()
pos := position(o)
if pos == nil {
return 0, badRequest("only rules, port forwards, source NAT rules and redirects have an order")
}
if rule, ok := o.(*model.Rule); ok && in.Chain != "" {
rule.Chain = in.Chain
}
chain := chainOf(o)
// The others in order.
var others []model.Object
for _, x := range st.Objects(op.Kind) {
if x.GetID() != id && chainOf(x) == chain {
others = append(others, x)
}
}
sort.SliceStable(others, func(i, j int) bool { return *position(others[i]) < *position(others[j]) })
place := func(i int) { // before others[i] (i == len: at the end)
switch {
case len(others) == 0:
*pos = 1
case i == 0:
*pos = *position(others[0]) - 1
case i == len(others):
*pos = *position(others[len(others)-1]) + 1
default:
*pos = (*position(others[i-1]) + *position(others[i])) / 2
}
}
index := func(ref int64) (int, error) {
for i, x := range others {
if x.GetID() == ref {
return i, nil
}
}
return 0, badRequest("%d isn't another %s of the same chain", ref, op.Kind)
}
switch {
case in.Before != 0:
i, err := index(in.Before)
if err != nil {
return 0, err
}
place(i)
case in.After != 0:
i, err := index(in.After)
if err != nil {
return 0, err
}
place(i + 1)
case in.To == "start":
place(0)
case in.To == "end" || in.Chain != "":
place(len(others))
default:
return 0, badRequest("say where: before, after, or to start or end")
}
data, _ := json.Marshal(o)
return id, st.Put(op.Kind, id, data)
}
func settingsOp(st *model.State, op *model.Op) error {
var patch map[string]json.RawMessage
if err := json.Unmarshal(op.Data, &patch); err != nil {
return badRequest("settings: %v", err)
}
var cur map[string]json.RawMessage
b, _ := json.Marshal(st.Settings)
json.Unmarshal(b, &cur)
for k, v := range patch {
// Built-ins merge one by one.
if k == "builtins" {
var cb, pb map[string]json.RawMessage
json.Unmarshal(cur[k], &cb)
if err := json.Unmarshal(v, &pb); err != nil {
return badRequest("builtins: %v", err)
}
for bk, bv := range pb {
cb[bk] = bv
}
v, _ = json.Marshal(cb)
}
cur[k] = v
}
merged, _ := json.Marshal(cur)
var next model.Settings
dec := json.NewDecoder(strings.NewReader(string(merged)))
dec.DisallowUnknownFields()
if err := dec.Decode(&next); err != nil {
return badRequest("settings: %v", err)
}
st.Settings = next
return nil
}
// replay applies a changeset's operations to st, in order. An operation
// that fails is named in the error (opError).
func replay(st *model.State, ops []model.Op) error {
for i := range ops {
if _, err := applyOp(st, &ops[i]); err != nil {
return &opError{seq: ops[i].Seq, summary: ops[i].Summary, err: err}
}
}
return nil
}
// opError: an operation of a changeset that can't be applied (the
// objects it names changed since it was staged, say).
type opError struct {
seq int
summary string
err error
}
func (e *opError) Error() string {
return fmt.Sprintf("operation %d (%s) can't be applied: %v", e.seq, e.summary, e.err)
}
func (e *opError) Unwrap() error { return e.err }
// info is the error for the API, with the inner error's status.
func (e *opError) info() (api.ErrorInfo, int) {
info := api.ErrorInfo{Code: "conflict", Message: e.Error(), Op: e.seq}
status := http.StatusConflict
var he *httpError
if errors.As(e.err, &he) {
info.Code, status = he.code, he.status
}
if ee, ok := engine.AsError(e.err); ok {
info.Code, info.Problems, status = ee.Kind, ee.Problems, engineStatus(ee.Kind)
}
return info, status
}
// objectOf picks what an operation's answer returns from a state.
func objectOf(op *model.Op, id int64) func(st *model.State) any {
switch op.Op {
case model.OpDelete:
return nil
case model.OpSettings:
return func(st *model.State) any { return st.Settings }
}
return func(st *model.State) any {
if o := st.ByID(op.Kind, id); o != nil {
return o
}
return nil
}
}