Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli command to validate configuration using plugin validation routines. #5626

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion cmd/spire-server/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func (cc *CLI) Run(ctx context.Context, args []string) int {
return jwt.NewMintCommand(), nil
},
"validate": func() (cli.Command, error) {
return validate.NewValidateCommand(), nil
return validate.NewValidateCommand(ctx, cc.LogOptions), nil
},
"localauthority x509 show": func() (cli.Command, error) {
return localauthority_x509.NewX509ShowCommand(), nil
Expand Down
48 changes: 39 additions & 9 deletions cmd/spire-server/cli/validate/validate.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
package validate

import (
"context"
"fmt"

"github.com/mitchellh/cli"
"github.com/spiffe/spire/cmd/spire-server/cli/run"
commoncli "github.com/spiffe/spire/pkg/common/cli"
"github.com/spiffe/spire/pkg/common/log"
"github.com/spiffe/spire/pkg/server"
)

const commandName = "validate"

func NewValidateCommand() cli.Command {
return newValidateCommand(commoncli.DefaultEnv)
func NewValidateCommand(ctx context.Context, logOptions []log.Option) cli.Command {
return newValidateCommand(ctx, commoncli.DefaultEnv, logOptions)
}

func newValidateCommand(env *commoncli.Env) *validateCommand {
func newValidateCommand(ctx context.Context, env *commoncli.Env, logOptions []log.Option) *validateCommand {
return &validateCommand{
env: env,
ctx: ctx,
env: env,
logOptions: logOptions,
}
}

type validateCommand struct {
env *commoncli.Env
ctx context.Context
logOptions []log.Option
env *commoncli.Env
}

// Help prints the server cmd usage
Expand All @@ -32,11 +41,32 @@ func (c *validateCommand) Synopsis() string {
}

func (c *validateCommand) Run(args []string) int {
if _, err := run.LoadConfig(commandName, args, nil, c.env.Stderr, false); err != nil {
// Ignore error since a failure to write to stderr cannot very well be reported
_ = c.env.ErrPrintf("SPIRE server configuration file is invalid: %v\n", err)
config, err := run.LoadConfig(commandName, args, c.logOptions, c.env.Stderr, false)
if err != nil {
_, _ = fmt.Fprintln(c.env.Stderr, err)
return 1
}
config.ValidateOnly = true

// Set umask before starting up the server
commoncli.SetUmask(config.Log)

s := server.New(*config)

ctx := c.ctx
if ctx == nil {
ctx = context.Background()
}

err = s.Run(ctx)
if err != nil {
config.Log.WithError(err).Error("Validation failed: validation server crashed")
return 1
}
_ = c.env.Println("SPIRE server configuration file is valid.")

fmt.Printf("status: %+v\n", config.ValidationNotes)
fmt.Printf("error: %+v\n", config.ValidationInError)

config.Log.Info("Validation server stopped gracefully")
return 0
}
80 changes: 64 additions & 16 deletions pkg/common/catalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,15 @@ type Config struct {

// CoreConfig is the core configuration provided to each plugin.
CoreConfig CoreConfig

// Validate plugins only
ValidateOnly bool
}

type Catalog struct {
closers io.Closer
reconfigurers Reconfigurers
closers io.Closer
reconfigurers Reconfigurers
validateResults map[string]*ValidateResult
}

func (c *Catalog) Reconfigure(ctx context.Context) {
Expand All @@ -130,6 +134,8 @@ func (c *Catalog) Close() error {
// configure, all plugins are unloaded, the catalog is cleared, and the
// function returns an error.
func Load(ctx context.Context, config Config, repo Repository) (_ *Catalog, err error) {
validations := make(map[string]*ValidateResult)
validations["catalog"] = NewValidateResult()
closers := make(closerGroup, 0)
defer func() {
// If loading fails, clear out the catalog and close down all plugins
Expand All @@ -145,57 +151,94 @@ func Load(ctx context.Context, config Config, repo Repository) (_ *Catalog, err
}
}()

validations["catalog"].ReportInfo("validating common catalog items")
log := config.Log.WithFields(logrus.Fields{
telemetry.SubsystemName: "common_catalog",
})

pluginRepos, err := makeBindablePluginRepos(repo.Plugins())
if err != nil {
return nil, err
}
log.Infof("bindablePluginRepos: %+v", pluginRepos)

serviceRepos, err := makeBindableServiceRepos(repo.Services())
if err != nil {
return nil, err
}
log.Infof("bindableServiceRepos: %+v", serviceRepos)

pluginCounts := make(map[string]int)
var reconfigurers Reconfigurers

for _, pluginConfig := range config.PluginConfigs {
validations["catalog"].ReportInfof("processing %s", pluginConfig.Name)
log.Infof("plugin(%s): processing", pluginConfig.Name)

pluginLog := makePluginLog(config.Log, pluginConfig)

pluginRepo, ok := pluginRepos[pluginConfig.Type]
if !ok {
pluginLog.Error("Unsupported plugin type")
return nil, fmt.Errorf("unsupported plugin type %q", pluginConfig.Type)
validations["catalog"].ReportErrorf("plugin(%s): Unsupported plugin type %s", pluginConfig.Name, pluginConfig.Type)
if !config.ValidateOnly {
return nil, fmt.Errorf("unsupported plugin type %q", pluginConfig.Type)
}
continue
}
log.Infof("plugin(%s): supported", pluginConfig.Name)

if pluginConfig.Disabled {
validations["catalog"].ReportErrorf("plugin(%s): Disabled", pluginConfig.Name)
pluginLog.Debug("Not loading plugin; disabled")
continue
}

plugin, err := loadPlugin(ctx, pluginRepo.BuiltIns(), pluginConfig, pluginLog, config.HostServices)
if err != nil {
validations["catalog"].ReportErrorf("plugin(%s): Failed to load plugin", pluginConfig.Name)
pluginLog.WithError(err).Error("Failed to load plugin")
return nil, fmt.Errorf("failed to load plugin %q: %w", pluginConfig.Name, err)
if !config.ValidateOnly {
return nil, fmt.Errorf("failed to load plugin %q: %w", pluginConfig.Name, err)
}
continue
}

// Add the plugin to the closers even though it has not been completely
// configured. If anything goes wrong (i.e. failure to configure,
// panic, etc.) we want the defer above to close the plugin. Failure to
// do so can orphan external plugin processes.
closers = append(closers, pluginCloser{plugin: plugin, log: pluginLog})
log.Infof("plugin(%s): loaded", pluginConfig.Name)

configurer, err := plugin.bindRepos(pluginRepo, serviceRepos)
if err != nil {
validations["catalog"].ReportErrorf("plugin(%s): Failed to bind plugin", pluginConfig.Name)
pluginLog.WithError(err).Error("Failed to bind plugin")
return nil, fmt.Errorf("failed to bind plugin %q: %w", pluginConfig.Name, err)
if !config.ValidateOnly {
return nil, fmt.Errorf("failed to bind plugin %q: %w", pluginConfig.Name, err)
}
}
log.Infof("plugin(%s): bound, configurer %+v", pluginConfig.Name, configurer)

reconfigurer, err := configurePlugin(ctx, pluginLog, config.CoreConfig, configurer, pluginConfig.DataSource)
if err != nil {
pluginLog.WithError(err).Error("Failed to configure plugin")
return nil, fmt.Errorf("failed to configure plugin %q: %w", pluginConfig.Name, err)
}
if reconfigurer != nil {
reconfigurers = append(reconfigurers, reconfigurer)
if !config.ValidateOnly {
reconfigurer, err := configurePlugin(ctx, pluginLog, config.CoreConfig, configurer, pluginConfig.DataSource)
if err != nil {
pluginLog.WithError(err).Error("Failed to configure plugin")
return nil, fmt.Errorf("failed to configure plugin %q: %w", pluginConfig.Name, err)
}
if reconfigurer != nil {
reconfigurers = append(reconfigurers, reconfigurer)
}
} else {
result, err := validatePlugin(ctx, pluginLog, config.CoreConfig, configurer, pluginConfig.DataSource)
if err != nil {
result.Valid = false
result.Notes = []string{fmt.Sprintf("error validating plugin %s", err)}
}
log.Infof("plugin(%s): validated", pluginConfig.Name)
log.Infof("plugin(%s): return data %+v", pluginConfig.Name, result)
log.Infof("plugin(%s): return err %s", pluginConfig.Name, err)
validations[pluginConfig.Name] = result
}

pluginLog.Info("Plugin loaded")
Expand All @@ -205,13 +248,18 @@ func Load(ctx context.Context, config Config, repo Repository) (_ *Catalog, err
// Make sure all of the plugin constraints are satisfied
for pluginType, pluginRepo := range pluginRepos {
if err := pluginRepo.Constraints().Check(pluginCounts[pluginType]); err != nil {
return nil, fmt.Errorf("plugin type %q constraint not satisfied: %w", pluginType, err)
validations["catalog"].ReportErrorf("plugin type (%q): Constraint violated: %w", pluginType, err)
if !config.ValidateOnly {
return nil, fmt.Errorf("plugin type %q constraint not satisfied: %w", pluginType, err)
}
continue
}
}

return &Catalog{
closers: closers,
reconfigurers: reconfigurers,
closers: closers,
reconfigurers: reconfigurers,
validateResults: validations,
}, nil
}

Expand Down
88 changes: 74 additions & 14 deletions pkg/common/catalog/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,37 @@ func (c CoreConfig) v1() *configv1.CoreConfiguration {

type Configurer interface {
Configure(ctx context.Context, coreConfig CoreConfig, configuration string) error
Validate(ctx context.Context, coreConfig CoreConfig, configuration string) error
Validate(ctx context.Context, coreConfig CoreConfig, configuration string) (*ValidateResult, error)
}

type ConfigurerFunc func(ctx context.Context, coreConfig CoreConfig, configuration string) error
type ValidateResult struct {
Valid bool
Notes []string
}

func NewValidateResult() *ValidateResult {
return &ValidateResult{
Valid: true,
Notes: nil,
}
}

func (vr *ValidateResult) ReportError(message string) {
vr.Valid = false
vr.Notes = append(vr.Notes, message)
}

func (vr *ValidateResult) ReportErrorf(format string, parameters ...any) {
vr.Valid = false
vr.Notes = append(vr.Notes, fmt.Sprintf(format, parameters...))
}

func (fn ConfigurerFunc) Configure(ctx context.Context, coreConfig CoreConfig, configuration string) error {
return fn(ctx, coreConfig, configuration)
func (vr *ValidateResult) ReportInfo(message string) {
vr.Notes = append(vr.Notes, message)
}

func (fn ConfigurerFunc) Validate(ctx context.Context, coreConfig CoreConfig, configuration string) error {
return fn(ctx, coreConfig, configuration)
func (vr *ValidateResult) ReportInfof(format string, parameters ...any) {
vr.Notes = append(vr.Notes, fmt.Sprintf(format, parameters...))
}

func ConfigurePlugin(ctx context.Context, coreConfig CoreConfig, configurer Configurer, dataSource DataSource, lastHash string) (string, error) {
Expand All @@ -57,6 +77,18 @@ func ConfigurePlugin(ctx context.Context, coreConfig CoreConfig, configurer Conf
return dataHash, nil
}

func ValidatePlugin(ctx context.Context, coreConfig CoreConfig, configurer Configurer, dataSource DataSource) (*ValidateResult, error) {
status := NewValidateResult()
data, err := dataSource.Load()
if err != nil {
status.ReportErrorf("failed to load plugin data: %w", err)
return status, err
}
status.ReportInfo("<validating plugin>")

return configurer.Validate(ctx, coreConfig, data)
}

func ReconfigureTask(log logrus.FieldLogger, reconfigurer Reconfigurer) func(context.Context) error {
return func(ctx context.Context) error {
return ReconfigureOnSignal(ctx, log, reconfigurer)
Expand Down Expand Up @@ -129,6 +161,25 @@ func configurePlugin(ctx context.Context, pluginLog logrus.FieldLogger, coreConf
}, nil
}

func validatePlugin(ctx context.Context, pluginLog logrus.FieldLogger, coreConfig CoreConfig, configurer Configurer, dataSource DataSource) (*ValidateResult, error) {
pluginLog.Info("validating plugin")
switch {
case configurer == nil && dataSource == nil:
// The plugin doesn't support configuration and no data source was configured. Nothing to do.
return NewValidateResult(), nil
case configurer == nil && dataSource != nil:
// The plugin does not support configuration but a data source was configured. This is a failure.
return NewValidateResult(), errors.New("no supported configuration interface found")
case configurer != nil && dataSource == nil:
// The plugin supports configuration but no data source was configured. Default to an empty, fixed configuration.
dataSource = FixedData("")
case configurer != nil && dataSource != nil:
// The plugin supports configuration and there was a data source.
}

return ValidatePlugin(ctx, coreConfig, configurer, dataSource)
}

type configurerRepo struct {
configurer Configurer
}
Expand Down Expand Up @@ -166,9 +217,6 @@ var _ Configurer = (*configurerV1)(nil)
func (v1 *configurerV1) InitInfo(PluginInfo) {
}

func (v1 *configurerV1) InitLog(logrus.FieldLogger) {
}

func (v1 *configurerV1) Configure(ctx context.Context, coreConfig CoreConfig, hclConfiguration string) error {
_, err := v1.ConfigServiceClient.Configure(ctx, &configv1.ConfigureRequest{
CoreConfiguration: coreConfig.v1(),
Expand All @@ -177,12 +225,22 @@ func (v1 *configurerV1) Configure(ctx context.Context, coreConfig CoreConfig, hc
return err
}

func (v1 *configurerV1) Validate(ctx context.Context, coreConfig CoreConfig, hclConfiguration string) error {
_, err := v1.ConfigServiceClient.Validate(ctx, &configv1.ValidateRequest{
func (v1 *configurerV1) Validate(ctx context.Context, coreConfig CoreConfig, hclConfiguration string) (*ValidateResult, error) {
response, err := v1.ConfigServiceClient.Validate(ctx, &configv1.ValidateRequest{
CoreConfiguration: coreConfig.v1(),
HclConfiguration: hclConfiguration,
})
return err
if err != nil {
return NewValidateResult(), err
}

result := NewValidateResult()
result.Valid = response.GetValid()
result.Notes = response.GetNotes()
return result, err
}

func (v1 *configurerV1) InitLog(logrus.FieldLogger) {
}

type configurerUnsupported struct{}
Expand All @@ -191,8 +249,10 @@ func (c configurerUnsupported) Configure(context.Context, CoreConfig, string) er
return status.Error(codes.FailedPrecondition, "plugin does not support a configuration interface")
}

func (c configurerUnsupported) Validate(context.Context, CoreConfig, string) error {
return status.Error(codes.FailedPrecondition, "plugin does not support a validation interface")
func (c configurerUnsupported) Validate(context.Context, CoreConfig, string) (*ValidateResult, error) {
result := NewValidateResult()
result.ReportInfo("plugin does not support a validation interface")
return result, status.Error(codes.FailedPrecondition, "plugin does not support a validation interface")
}

func hashData(data string) string {
Expand Down
Loading