Introduce a commercial license option alongside AGPL-3.0-only, require a CLA for contributors, and document the terms in COMMERCIAL.md and NOTICE. Add a script to stamp SPDX headers on Go files and apply it across the tree.
112 lines
4.5 KiB
Go
112 lines
4.5 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/config"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/db"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/integration"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/logging"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// validateConfigCmd is the pre-flight check design D10
|
|
// (typed-config-keys) names: every source in that design's research puts a
|
|
// test between an edit and the process that applies it (consul validate,
|
|
// nginx -t, systemd-analyze verify), so a deployer runs this before an
|
|
// upgrade's restart and a bad value surfaces in their terminal instead of
|
|
// the service manager's restart loop. It reuses the exact boot path (the
|
|
// same CheckSpecs, resolveSecretFiles, ValidateStart and ApplyOverlay
|
|
// start.go calls) and adds no rule of its own; it never starts the server.
|
|
// It is a subcommand of config, beside list, set and clear: the terminal
|
|
// is the deployer's door and its convention is config (git config, kubectl
|
|
// config, npm config), while the product's pages say settings
|
|
// (docs/settings-and-configuration.md).
|
|
var validateConfigCmd = &cobra.Command{
|
|
Use: "validate",
|
|
Short: "Check the configuration the next boot will read, without starting the server",
|
|
Long: `config validate runs the checks member-console start runs before it
|
|
initializes any service, in the same order: every installed integration's
|
|
own declarations, the resolved environment, flag and config-file values,
|
|
and every stored override, each parsed against its key's declared type,
|
|
through the same parser the settings page and the boot overlay use. It
|
|
connects to the database to read the stored overrides and nothing else: it
|
|
never runs migrations and never starts the server.
|
|
|
|
Exits 0 with one line when everything parses. Exits 1 naming the first
|
|
failure and its remediation otherwise.`,
|
|
Args: cobra.NoArgs,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
var specs []config.ConfigKey
|
|
for _, integ := range integrations.All() {
|
|
if cp, ok := integ.(config.ConfigProvider); ok {
|
|
specs = append(specs, cp.ConfigSpec()...)
|
|
}
|
|
}
|
|
ok, message := runValidateConfig(context.Background(), specs)
|
|
if !ok {
|
|
fmt.Fprintln(os.Stderr, message)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println(message)
|
|
},
|
|
}
|
|
|
|
// runValidateConfig performs every check validate-config runs and returns
|
|
// the message to print and whether every check passed, so a test can
|
|
// assert on the outcome without exercising os.Exit. specs is the caller's
|
|
// installed-integration ConfigSpec aggregate (production: integrations.All()
|
|
// filtered to config.ConfigProvider; tests: a fixture spec, the same
|
|
// substitution ValidateStart's and CheckSpecs' own tests make).
|
|
func runValidateConfig(ctx context.Context, specs []config.ConfigKey) (ok bool, message string) {
|
|
if err := config.CheckSpecs(specs); err != nil {
|
|
return false, fmt.Sprintf("config validate: %v", err)
|
|
}
|
|
if err := resolveSecretFiles(specs); err != nil {
|
|
return false, fmt.Sprintf("config validate: %v", err)
|
|
}
|
|
if err := config.ValidateStart(specs); err != nil {
|
|
return false, fmt.Sprintf("config validate: %v", err)
|
|
}
|
|
|
|
logger := slog.Default()
|
|
if logging.AppLogger != nil {
|
|
logger = logging.AppLogger
|
|
}
|
|
dbConfig := db.DefaultDBConfig(viper.GetString("db-dsn"))
|
|
database, err := db.ConnectPlain(ctx, logger, dbConfig)
|
|
if err != nil {
|
|
return false, fmt.Sprintf("config validate: connecting to the database: %v", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
overrideRows, err := integration.New(database).ListConfigOverrides(ctx)
|
|
if err != nil {
|
|
return false, fmt.Sprintf("config validate: loading stored overrides: %v", err)
|
|
}
|
|
overrides := make([]config.Override, 0, len(overrideRows))
|
|
for _, row := range overrideRows {
|
|
overrides = append(overrides, config.Override{Key: row.Key, Value: row.Value})
|
|
}
|
|
// ApplyOverlay is the exact boot-time overlay (design D10, "adds no rule
|
|
// of its own"): a secret-key override, or a stored value that fails to
|
|
// parse, fails here with the same key and remediation start.go's own
|
|
// call would produce.
|
|
if err := config.ApplyOverlay(specs, overrides); err != nil {
|
|
return false, fmt.Sprintf("config validate: %v", err)
|
|
}
|
|
return true, "config validate: ok"
|
|
}
|
|
|
|
func init() {
|
|
settingsCmd.AddCommand(validateConfigCmd)
|
|
}
|