Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
95 lines
4.2 KiB
Go
95 lines
4.2 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
)
|
|
|
|
// ErrInvalidSettings wraps a settings document that failed validation; the running configuration is unchanged.
|
|
var ErrInvalidSettings = errors.New("invalid settings")
|
|
|
|
// SettingsResult says what a settings change did.
|
|
type SettingsResult struct {
|
|
// Applied names the sections that took effect in the running process.
|
|
Applied []string
|
|
// RestartRequired names the changed sections that only take effect after a restart.
|
|
RestartRequired []string
|
|
}
|
|
|
|
// SettingsAdmin reads config.yaml and replaces it. Apply validates the whole document first and changes
|
|
// nothing when it is invalid (the error wraps ErrInvalidSettings); otherwise it writes the file atomically and
|
|
// reloads the parts that can change while nmon runs.
|
|
type SettingsAdmin interface {
|
|
// Current is the configuration as YAML and the sections edited since start that still need a restart.
|
|
Current(ctx context.Context) (yaml string, restartRequired []string, err error)
|
|
Apply(ctx context.Context, actor, yaml string) (SettingsResult, error)
|
|
}
|
|
|
|
// SettingsView is the configuration document.
|
|
type SettingsView struct {
|
|
YAML string `json:"yaml" doc:"config.yaml. Secrets are never in it; the OIDC client secret is a named secret."`
|
|
RestartRequired []string `json:"restart_required" doc:"Sections whose saved value differs from what is running and needs a restart."`
|
|
}
|
|
|
|
// SettingsBody replaces the configuration document.
|
|
type SettingsBody struct {
|
|
YAML string `json:"yaml" minLength:"1" maxLength:"1048576" doc:"The complete new config.yaml."`
|
|
}
|
|
|
|
// SettingsApplied reports the outcome of PUT /settings.
|
|
type SettingsApplied struct {
|
|
YAML string `json:"yaml" doc:"The configuration as saved."`
|
|
Applied []string `json:"applied" doc:"Sections reloaded in place."`
|
|
RestartRequired []string `json:"restart_required" doc:"Sections saved but running with the old value until a restart: listen address, TLS, data dir, retention, rules, checks, Proxmox, timezone."` //nolint:lll // struct tag text is the OpenAPI doc; cannot wrap
|
|
}
|
|
|
|
func (s *server) registerSettings(api huma.API) {
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "get-settings", Method: http.MethodGet, Path: "/api/v1/settings", Tags: []string{"settings"},
|
|
Summary: "Read the configuration", Description: "Admin only. The effective config.yaml.",
|
|
Errors: []int{http.StatusServiceUnavailable},
|
|
}, s.getSettings)
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "put-settings", Method: http.MethodPut, Path: "/api/v1/settings", Tags: []string{"settings"},
|
|
Summary: "Replace the configuration",
|
|
Description: "Admin only. Validates the whole document, writes config.yaml atomically and reloads what can change " +
|
|
"without a restart. An invalid document is a 422 and nothing changes. SIGHUP reloads the file the same way.",
|
|
Errors: []int{http.StatusUnprocessableEntity, http.StatusServiceUnavailable},
|
|
}, s.putSettings)
|
|
}
|
|
|
|
func (s *server) getSettings(ctx context.Context, _ *struct{}) (*struct{ Body SettingsView }, error) {
|
|
if s.Settings == nil {
|
|
return nil, errNotConfigured("settings")
|
|
}
|
|
y, pending, err := s.Settings.Current(ctx)
|
|
if err != nil {
|
|
s.Logger.Error("read settings", "err", err)
|
|
return nil, huma.Error500InternalServerError("could not read the configuration; see the log")
|
|
}
|
|
return &struct{ Body SettingsView }{SettingsView{YAML: y, RestartRequired: nonNil(pending)}}, nil
|
|
}
|
|
|
|
func (s *server) putSettings(ctx context.Context, in *struct{ Body SettingsBody }) (*struct{ Body SettingsApplied }, error) {
|
|
if s.Settings == nil {
|
|
return nil, errNotConfigured("settings")
|
|
}
|
|
res, err := s.Settings.Apply(ctx, actorOf(ctx), in.Body.YAML)
|
|
if errors.Is(err, ErrInvalidSettings) {
|
|
return nil, huma.Error422UnprocessableEntity(trimSentinel(err, ErrInvalidSettings))
|
|
}
|
|
if err != nil {
|
|
s.Logger.Error("apply settings", "err", err)
|
|
return nil, huma.Error500InternalServerError("could not save the configuration; see the log")
|
|
}
|
|
y, _, err := s.Settings.Current(ctx)
|
|
if err != nil {
|
|
s.Logger.Warn("read back settings", "err", err)
|
|
}
|
|
out := SettingsApplied{YAML: y, Applied: nonNil(res.Applied), RestartRequired: nonNil(res.RestartRequired)}
|
|
return &struct{ Body SettingsApplied }{out}, nil
|
|
}
|