Files
nmon/internal/api/settings.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
}