diff --git a/backend/cli/app.go b/backend/cli/app.go new file mode 100644 index 000000000..fd12ecf22 --- /dev/null +++ b/backend/cli/app.go @@ -0,0 +1,135 @@ +package cli + +import ( + "fmt" + + flags "github.com/jessevdk/go-flags" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "isc.org/stork" + storkutil "isc.org/stork/util" +) + +// It specifies a method that checks if the specific command was specified in +// the CLI. It is used to create a mapping between the command objects and +// the command handlers. +type command interface { + isSpecified() bool +} + +// The struct that must be embedded in all structures defining the command +// settings. It allows to recognize which command was specified in the CLI. +// It is related to how the go-flags library handles the subcommands. +// +// It may be also used to specify arguments of the command that accepts no +// arguments. +type CommandSettings struct { + // It is true if the register command was specified. Otherwise, it is false. + commandSpecified bool +} + +// Checks if the struct implement the library interface. +var _ flags.Commander = (*CommandSettings)(nil) + +// Implements the tools/golang/gopath/pkg/mod/github.com/jessevdk/go-flags@v1.5.0/command.go Commander interface. +// It is an only way to recognize which command was specified. +func (s *CommandSettings) Execute(_ []string) error { + s.commandSpecified = true + return nil +} + +// Indicates if the command was specified. +func (s *CommandSettings) isSpecified() bool { + return s.commandSpecified +} + +// Prints the Stork version. +func showVersion() { + fmt.Println(stork.Version) +} + +// The type describing the command handler. +// It is a function that takes no arguments and returns no value. +// Maybe it should an error as a return value. Currently, it is not necessary +// but it may be useful in the future refactorings for example to unify the +// error handling in various commands. +type action = func() + +// A helper structure that mimics the behavior of the urfave/cli/v2 package. +// It helps to create a CLI application with subcommands. +// It is a wrapper around the go-flags library. It accepts the go-flags parser +// and provides a convenient way to create subcommands that should be used +// instead of the built-in .AddCommand method. +// It has a run method that parses the command line arguments and executes +// the appropriate action. +type App struct { + commandsToFunctions map[command]action + parser *flags.Parser + showVersion bool +} + +// Constructs a new application instance. +func NewApp(parser *flags.Parser) *App { + app := &App{ + commandsToFunctions: make(map[command]action), + parser: parser, + } + app.enableVersionOption() + return app +} + +// Adds a top-level CLI argument to show the software version. +func (a *App) enableVersionOption() { + a.parser.Group.AddOption(&flags.Option{ + ShortName: 'v', + LongName: "version", + Description: "Show software version", + }, &a.showVersion) +} + +// Registers a command with the parser and associates it with the action. +func (a *App) RegisterCommand(command, shortDescription string, data command, action action) { + _, err := a.parser.AddCommand(command, shortDescription, "", data) + if err != nil { + logrus.WithError(err).Fatal("Failed to add command") + } + a.commandsToFunctions[data] = action +} + +// Starts the application with the provided arguments. +// Run requested subcommand or show help or version. +func (a *App) Run(application string, args []string) error { + // Parse command line arguments. + appParser := NewCLIParser(a.parser, application, func() { + storkutil.SetupLogging() + }) + + _, _, isHelp, err := appParser.Parse(args) + if err != nil { + return err + } + if isHelp { + return nil + } + + // Handle the version argument first. + if a.showVersion { + showVersion() + return nil + } + + // Find the command that was specified. + for command, action := range a.commandsToFunctions { + if command.isSpecified() { + action() + return nil + } + } + + var availableCommands []string + for _, command := range a.parser.Commands() { + availableCommands = append(availableCommands, command.Name) + } + + return errors.Errorf("no command specified, available commands: %v", availableCommands) +} diff --git a/backend/cli/app_test.go b/backend/cli/app_test.go new file mode 100644 index 000000000..e600763ee --- /dev/null +++ b/backend/cli/app_test.go @@ -0,0 +1,140 @@ +package cli + +import ( + "fmt" + "strings" + "testing" + + flags "github.com/jessevdk/go-flags" + "github.com/stretchr/testify/require" + "isc.org/stork" + "isc.org/stork/testutil" +) + +// Test that the application instance is created properly. +func TestNewApp(t *testing.T) { + // Arrange + parser := flags.NewParser(&struct{}{}, flags.Default) + + // Act + app := NewApp(parser) + + // Assert + require.NotNil(t, app) + require.NotNil(t, app.commandsToFunctions) + require.NotNil(t, app.parser) + require.False(t, app.showVersion) + require.Equal(t, parser, app.parser) +} + +// Test that the version printing is handled internally. +func TestRunVersion(t *testing.T) { + // Arrange + parser := flags.NewParser(&struct{}{}, flags.Default) + app := NewApp(parser) + + for _, arg := range []string{"-v", "--version"} { + t.Run(arg, func(t *testing.T) { + var err error + + // Act + stdout, _, _ := testutil.CaptureOutput(func() { + err = app.Run("agent", []string{arg}) + }) + + // Assert + require.NoError(t, err) + require.True(t, app.showVersion) + require.Equal(t, stork.Version, strings.TrimSpace(string(stdout))) + }) + } +} + +// Test that the help printing is handled internally. +// Check if the hook directory is shown for agent and server. +func TestRunHelp(t *testing.T) { + // Arrange + for _, name := range []string{"tool", "agent", "server", "code-gen", "unknown"} { + for _, arg := range []string{"-h", "--help"} { + parser := flags.NewParser(&struct{}{}, flags.Default) + parser.Name = "foo" + parser.ShortDescription = "Bar" + parser.LongDescription = "Baz" + app := NewApp(parser) + + t.Run(fmt.Sprintf("%s/%s", name, arg), func(t *testing.T) { + // Act + var err error + stdout, _, _ := testutil.CaptureOutput(func() { + err = app.Run(name, []string{arg}) + }) + + // Assert + require.NoError(t, err) + require.Contains(t, string(stdout), "foo") + require.NotContains(t, string(stdout), "Bar") + require.Contains(t, string(stdout), "Baz") + require.Contains(t, string(stdout), "--help") + require.Contains(t, string(stdout), "--version") + + if name == "agent" || name == "server" { + require.Contains(t, string(stdout), "--hook-directory") + } else { + require.NotContains(t, string(stdout), "--hook-directory") + } + }) + } + } +} + +// Test that the error is returned when the command is not provided. +func TestRunNoCommand(t *testing.T) { + // Arrange + parser := flags.NewParser(&struct{}{}, flags.Default) + app := NewApp(parser) + + // Act + err := app.Run("server", []string{}) + + // Assert + require.ErrorContains(t, err, "no command specified") + require.ErrorContains(t, err, "available commands:") +} + +// Test that the error is returned when the command is not recognized. +func TestRunUnknownCommand(t *testing.T) { + // Arrange + parser := flags.NewParser(&struct{}{}, flags.Default) + app := NewApp(parser) + + // Act + err := app.Run("agent", []string{"unknown"}) + + // Assert + require.ErrorContains(t, err, "no command specified") + require.ErrorContains(t, err, "available commands:") +} + +// Test that the command is executed when it is recognized. +func TestRunCommand(t *testing.T) { + // Arrange + parser := flags.NewParser(&struct{}{}, flags.Default) + app := NewApp(parser) + type settings struct { + CommandSettings + Foo string `short:"f" long:"foo" description:"Foo"` + } + data := &settings{} + isCalled := false + + // Act + app.RegisterCommand("bar", "Bar", data, func() { + isCalled = true + }) + err := app.Run("tool", []string{"bar", "-f", "baz"}) + + // Assert + require.NoError(t, err) + require.True(t, isCalled) + require.Equal(t, "baz", data.Foo) +} diff --git a/backend/cli/cli.go b/backend/cli/cli.go new file mode 100644 index 000000000..743b44d37 --- /dev/null +++ b/backend/cli/cli.go @@ -0,0 +1,492 @@ +package cli + +import ( + "fmt" + "os" + "strings" + + flags "github.com/jessevdk/go-flags" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "isc.org/stork/hooks" + "isc.org/stork/hooksutil" + storkutil "isc.org/stork/util" +) + +// Read environment file settings. It's parsed before the main settings. +type environmentFileSettings struct { + EnvFile string `long:"env-file" description:"Environment file location; applicable only if the use-env-file is provided" default:"/etc/stork/@APP.env"` + UseEnvFile bool `long:"use-env-file" description:"Read the environment variables from the environment file"` +} + +// Read hook directory settings. They are parsed after environment file +// settings but before the main settings. +// It allows us to merge the hook flags with the core flags into a single output. +type HookDirectorySettings struct { + HookDirectory string `long:"hook-directory" description:"The path to the hook directory" env:"STORK_@APP_HOOK_DIRECTORY" default:"/usr/lib/stork-@APP/hooks"` +} + +// Defines the type for set of hook settings grouped by the hook name. +type GroupedHookCLIFlags map[string]hooks.HookSettings + +// Stork server-specific CLI arguments/flags parser. +type CLIParser struct { + parser *flags.Parser + application string + onLoadEnvironmentFileCallback func() +} + +// Constructs CLI parser. +// Accepts the main parser. It should be configured with the application +// settings. +// The application name is used to construct the namespaces for the CLI flags +// and environment variables. +// The callback is called when the environment file is loaded. Its purpose is +// to allow reconfiguring the logging using the new environment variables as +// soon as they are available. +func NewCLIParser(parser *flags.Parser, app string, onLoadEnvironmentFileCallback func()) *CLIParser { + return &CLIParser{ + parser: parser, + application: strings.ToLower(app), + onLoadEnvironmentFileCallback: onLoadEnvironmentFileCallback, + } +} + +// Parse the command line arguments into Stork-specific GO structures. +// At the end, it composes the CLI parser from all the flags and runs it. +// Returns a hook directory settings, hook settings extracted from the hooks, +// flag indication if the help was requested and an error if any. +// Accepts the command line arguments. The passed arguments should exclude +// the application name (the first argument). +func (p *CLIParser) Parse(args []string) (*HookDirectorySettings, GroupedHookCLIFlags, bool, error) { + hookDirectorySettings, allHookFLags, err := p.bootstrap(args) + if err != nil { + if isHelpRequest(err) { + return nil, nil, true, nil + } + return nil, nil, false, err + } + + err = p.parse(args) + if err != nil { + if isHelpRequest(err) { + return nil, nil, true, nil + } + return nil, nil, false, err + } + return hookDirectorySettings, allHookFLags, false, nil +} + +// Parse the CLI flags stored in the main parser. +func (p *CLIParser) parse(args []string) (err error) { + // Do args parsing. + if _, err = p.parser.ParseArgs(args); err != nil { + err = errors.Wrap(err, "cannot parse the CLI flags") + return err + } + + return nil +} + +// It parses the settings related to an environment file and if the file +// is provided, the content is loaded. +// Next, it parses the hooks location and extracts their CLI flags. +// The hook flags are then merged with the core flags. +func (p *CLIParser) bootstrap(args []string) (*HookDirectorySettings, GroupedHookCLIFlags, error) { + // Environment variables. + envFileSettings := &environmentFileSettings{} + envParser := p.createSubParser(envFileSettings) + if _, err := envParser.ParseArgs(args); err != nil { + return nil, nil, err + } + err := p.loadEnvironmentFile(envFileSettings) + if err != nil { + return nil, nil, err + } + + // Process the hook directory location. + var hookDirectorySettings *HookDirectorySettings + var allHookCLIFlags GroupedHookCLIFlags + if p.application == "server" || p.application == "agent" { + hookDirectorySettings = &HookDirectorySettings{} + hookParser := p.createSubParser(hookDirectorySettings) + if _, err := hookParser.ParseArgs(args); err != nil { + return nil, nil, err + } + + allHookCLIFlags, err = p.collectHookCLIFlags(hookDirectorySettings) + if err != nil { + return nil, nil, err + } + err = p.mergeHookFlags(allHookCLIFlags) + if err != nil { + return nil, nil, err + } + + // Append the hook directory-related flags to the main parser. + group, err := p.parser.AddGroup("Hook Directory Flags", "", hookDirectorySettings) + if err != nil { + err = errors.Wrap(err, "cannot add the hook directory group") + return nil, nil, err + } + p.substitutePlaceholdersInGroup(group) + } + + // Append the environment file-related flags to the main parser. + group, err := p.parser.AddGroup("Environment File Flags", "", envFileSettings) + if err != nil { + err = errors.Wrap(err, "cannot add the environment file group") + return nil, nil, err + } + p.substitutePlaceholdersInGroup(group) + + // Verify the environment variables. + err = p.verifyEnvironmentFile(envFileSettings) + if err != nil { + return nil, nil, err + } + + p.verifySystemEnvironmentVariables() + + return hookDirectorySettings, allHookCLIFlags, nil +} + +// Merges the CLI flags of the hooks with the core CLI flags. +func (p *CLIParser) mergeHookFlags(allHooksCLIFlags map[string]hooks.HookSettings) error { + // Append hook flags. + for hookName, cliFlags := range allHooksCLIFlags { + if cliFlags == nil { + continue + } + group, err := p.parser.AddGroup(fmt.Sprintf("Hook '%s' Flags", hookName), "", cliFlags) + if err != nil { + err = errors.Wrapf(err, "invalid settings for the '%s' hook", hookName) + return err + } + + flagNamespace, envNamespace := getHookNamespaces(p.application, hookName) + group.EnvNamespace = envNamespace + group.Namespace = flagNamespace + } + + // Check if there are no two groups with the same namespace. + // It may happen if the one of the hooks has the expected common prefix, + // but another one doesn't. For example, if we have two hooks named: + // - stork-server-ldap + // - ldap + // Both of them will have the same namespace: ldap. + // We suppose it will be a rare case, so we just return an error. + groupNamespaces := make(map[string]any) + for _, group := range p.parser.Groups() { + if group.Namespace == "" { + // Non-hook group. Skip. + continue + } + _, exist := groupNamespaces[group.Namespace] + if exist { + return errors.Errorf( + "There are two hooks using the same configuration namespace "+ + "in the CLI flags: '%s'. The hook libraries for the "+ + "Stork server should use the following naming pattern, "+ + "e.g. 'stork-server-%s.so' instead of just '%s.so'", + group.Namespace, group.Namespace, group.Namespace, + ) + } + groupNamespaces[group.Namespace] = nil + } + + return nil +} + +// Check if a given error is a request to display the help. +func isHelpRequest(err error) bool { + var flagsError *flags.Error + if errors.As(err, &flagsError) { + if flagsError.Type == flags.ErrHelp { + return true + } + } + return false +} + +// Creates a new parser for the CLI flags that parsers a set of flags related +// to the CLI parsing itself rather than the application settings and parses +// the flags specified in the tags of the provided structure. +// It inherits the descriptions from the main parser and substitutes the +// placeholders in the defaults and environment variable names. +func (p *CLIParser) createSubParser(settings any) *flags.Parser { + parser := flags.NewParser(settings, flags.IgnoreUnknown) + + p.substitutePlaceholders(parser) + + parser.Name = p.parser.Name + parser.ShortDescription = p.parser.ShortDescription + parser.LongDescription = p.parser.LongDescription + return parser +} + +// Substitutes the placeholders in the defaults and environment variable names. +func (p *CLIParser) substitutePlaceholders(parser *flags.Parser) { + for _, group := range parser.Groups() { + p.substitutePlaceholdersInGroup(group) + } +} + +// Substitutes the placeholders in the defaults and environment variable names +// in a group of flags. +func (p *CLIParser) substitutePlaceholdersInGroup(group *flags.Group) { + for _, option := range group.Options() { + // Defaults. + for i, d := range option.Default { + option.Default[i] = strings.Replace(d, "@APP", p.application, 1) + } + + // Environment variables. + option.EnvDefaultKey = strings.Replace( + option.EnvDefaultKey, + "@APP", + strings.ToUpper(p.application), + 1, + ) + } +} + +// Loads the environment file content to the environment dictionary of the +// current process. +func (p *CLIParser) loadEnvironmentFile(envFileSettings *environmentFileSettings) error { + if !envFileSettings.UseEnvFile { + // Nothing to do. + return nil + } + + err := storkutil.LoadEnvironmentFileToSetter( + envFileSettings.EnvFile, + storkutil.NewProcessEnvironmentVariableSetter(), + ) + if err != nil { + err = errors.WithMessagef(err, "invalid environment file: '%s'", envFileSettings.EnvFile) + return err + } + + // Call the callback when the environment file is loaded. It allows to + // reconfigure the logging using the new environment variables. + p.onLoadEnvironmentFileCallback() + + return nil +} + +// Verifies if all environment variables in the environment file are known. +// Prints a warning if any of the environment variables is unknown. +func (p *CLIParser) verifyEnvironmentFile(envFileSettings *environmentFileSettings) error { + if !envFileSettings.UseEnvFile { + // Nothing to do. + return nil + } + + // Load the environment file content. + entries, err := storkutil.LoadEnvironmentFile(envFileSettings.EnvFile) + if err != nil { + err = errors.WithMessagef(err, "invalid environment file: '%s'", envFileSettings.EnvFile) + return err + } + + knownEnvironmentVariables := collectKnownEnvironmentVariables(p.parser.Command) + + // Check if all environment variables are known. + for key := range entries { + if _, exist := knownEnvironmentVariables[key]; !exist { + log.Warnf( + "Unknown environment variable: '%s' in the environment file: '%s'", + key, envFileSettings.EnvFile, + ) + } + } + + return nil +} + +// Verifies if the system-wide environment variables doesn't contain any +// unknown Stork-specific environment variables. +func (p *CLIParser) verifySystemEnvironmentVariables() { + // Collect all known environment variables. + knownEnvironmentVariables := collectKnownEnvironmentVariables(p.parser.Command) + + // Contains the prefixes of the Stork-specific environment variables. + // Stork environment variables starts with the 'STORK' part and then + // the context-specific part e.g.: + // + // - STORK_SERVER (server-specific environment variable) + // - STORK_AGENT (agent-specific environment variable) + // - STORK_DATABASE (database-specific environment variable) + // + // This function analyzes the system-wide environment variables. We cannot + // assume that there are only environment variables related to one of the + // Stork components. It should be allowed to have the agent, server, and + // tool environment variables set in the same shell. + // + // The below code block allows us to ignore the environment variables from + // other Stork components. There is an assumption that all components have + // exactly the same environment variables for a given prefix/namespace. + // For example, if the application utilizes the environment variables + // prefixed with 'STORK_DATABASE_' then its settings specify exactly the + // same environment variables as the other components. + // I hope it is fair enough. + var prefixes []string + + for environmentVariable := range knownEnvironmentVariables { + parts := strings.SplitN(environmentVariable, "_", 3) + if len(parts) < 3 { + // The environment variable doesn't have the context-specific part. + // The naming convention is violated. + continue + } + if parts[0] != "STORK" { + // The environment variable doesn't start with the 'STORK' part. + // The naming convention is violated. + continue + } + + prefixes = append(prefixes, fmt.Sprintf("%s_%s_", parts[0], parts[1])) + } + + // Iterate over all system-wide environment variables. + for _, env := range os.Environ() { + key, _, ok := strings.Cut(env, "=") + if !ok { + // It should never happen. + continue + } + + var isApplicationEnvironmentVariable bool + for _, prefix := range prefixes { + if strings.HasPrefix(key, prefix) { + isApplicationEnvironmentVariable = true + break + } + } + if !isApplicationEnvironmentVariable { + // Not a Stork-specific environment variable. + continue + } + + if _, exist := knownEnvironmentVariables[key]; exist { + // Known environment variable. + continue + } + + log.Warnf("Unknown environment variable: '%s' set in a shell", key) + } +} + +// Extracts the CLI flags from the hooks. +func (p *CLIParser) collectHookCLIFlags(hookDirectorySettings *HookDirectorySettings) (map[string]hooks.HookSettings, error) { + allCLIFlags := map[string]hooks.HookSettings{} + stat, err := os.Stat(hookDirectorySettings.HookDirectory) + switch { + case err == nil && stat.IsDir(): + // Gather the hook flags. + hookWalker := hooksutil.NewHookWalker() + var program string + switch p.application { + case "server": + program = hooks.HookProgramServer + case "agent": + program = hooks.HookProgramAgent + default: + // Programming error. + return nil, errors.Errorf("unknown application name: %s", p.application) + } + + allCLIFlags, err = hookWalker.CollectCLIFlags( + program, + hookDirectorySettings.HookDirectory, + ) + if err != nil { + err = errors.WithMessage(err, "cannot collect the prototypes of the hook settings") + return nil, err + } + case err == nil && !stat.IsDir(): + // Hook directory is not a directory. + err = errors.Errorf( + "the provided hook directory path is not pointing to a directory: %s", + hookDirectorySettings.HookDirectory, + ) + return nil, err + case errors.Is(err, os.ErrNotExist): + // Hook directory doesn't exist. Skip and continue. + break + default: + // Unexpected problem. + err = errors.Wrapf(err, + "cannot stat the hook directory: %s", + hookDirectorySettings.HookDirectory, + ) + return nil, err + } + + return allCLIFlags, nil +} + +// Prepare conventional namespaces for the CLI flags and environment +// variables. +// Accepts the application (i.e., 'server' or 'agent') and the hook name. +// +// CLI flags: +// - Have a component derived from the hook filename +// - Contains none upper cases, dots or spaces +// - Underscores are replaced with dashes +// +// Environment variables: +// - Starts with Stork-specific prefix +// - Have a component derived from the hook filename +// - Contains none lower cases, dots or spaces +// - Dashes are replaced with underscored +func getHookNamespaces(application string, hookName string) (flagNamespace, envNamespace string) { + // Trim the app-specific prefix for simplicity. + prefix := fmt.Sprintf("stork-%s-", application) + hookName, _ = strings.CutPrefix(hookName, prefix) + + // Replace all invalid characters with dashes. + hookName = strings.ReplaceAll(hookName, " ", "-") + hookName = strings.ReplaceAll(hookName, ".", "-") + + flagNamespace = strings.ReplaceAll(hookName, "_", "-") + flagNamespace = strings.ToLower(flagNamespace) + + // Prepend the common prefix for environment variables. + envNamespace = fmt.Sprintf( + "STORK_%s_HOOK_%s", + application, + strings.ReplaceAll(hookName, "-", "_"), + ) + envNamespace = strings.ToUpper(envNamespace) + return +} + +// Collects all known environment variables from a parser. Returns a set of +// the full environment variable names. +func collectKnownEnvironmentVariables(parser *flags.Command) map[string]bool { + knownEnvironmentVariables := make(map[string]bool) + + // The options of the main group of the top-level parser. + for _, option := range parser.Group.Options() { + knownEnvironmentVariables[option.EnvKeyWithNamespace()] = true + } + + // The groups of the top-level parser. + for _, group := range parser.Groups() { + for _, option := range group.Options() { + knownEnvironmentVariables[option.EnvKeyWithNamespace()] = true + } + } + + // The subcommands of the top-level parser. + for _, subcommand := range parser.Commands() { + subcommandEnvironmentVariables := collectKnownEnvironmentVariables(subcommand) + for key := range subcommandEnvironmentVariables { + knownEnvironmentVariables[key] = true + } + } + + return knownEnvironmentVariables +} diff --git a/backend/cli/cli_test.go b/backend/cli/cli_test.go new file mode 100644 index 000000000..e2084d0be --- /dev/null +++ b/backend/cli/cli_test.go @@ -0,0 +1,740 @@ +package cli + +import ( + "fmt" + "os" + "path" + "testing" + + "github.com/jessevdk/go-flags" + "github.com/stretchr/testify/require" + "isc.org/stork/hooks" + "isc.org/stork/testutil" +) + +// Test that the CLI parser is constructed properly. +func TestNewCLIParser(t *testing.T) { + // Arrange & Act + parser := NewCLIParser(nil, "server", func() {}) + + // Assert + require.NotNil(t, parser) + require.Nil(t, parser.parser) + require.Equal(t, "server", parser.application) + require.NotNil(t, parser.onLoadEnvironmentFileCallback) +} + +// Test that the environment variables from the environment file are loaded +// and parsed by the CLI parser. +func TestEnvironmentFileIsLoaded(t *testing.T) { + // Arrange + restorePoint := testutil.ClearEnvironmentVariables() + defer restorePoint() + sandbox := testutil.NewSandbox() + defer sandbox.Close() + + envPath, _ := sandbox.Write("file.env", ` + STORK_DATABASE_HOST=foo + STORK_SERVER_HOOK_DIRECTORY=bar + STORK_REST_HOST=baz + `) + + args := []string{ + "--use-env-file", + "--env-file", envPath, + } + + type settings struct { + DBHost string `long:"db-host" description:"The host name, IP address or socket where database is available" env:"STORK_DATABASE_HOST" default:""` + RESTHost string `long:"rest-host" description:"The IP to listen on" default:"" env:"STORK_REST_HOST"` + } + + data := &settings{} + + flagParser := flags.NewParser(data, flags.Default) + + parser := NewCLIParser(flagParser, "server", func() {}) + + // Act + hookDirSettings, hookFlags, isHelp, err := parser.Parse(args) + + // Assert + require.NoError(t, err) + require.False(t, isHelp) + require.Empty(t, hookFlags) + + require.Equal(t, "foo", data.DBHost) + require.Equal(t, "bar", hookDirSettings.HookDirectory) + require.Equal(t, "baz", data.RESTHost) +} + +// Test that the error is returned if the environment file is invalid. +func TestEnvironmentFileIsInvalid(t *testing.T) { + // Arrange + restorePoint := testutil.ClearEnvironmentVariables() + defer restorePoint() + sandbox := testutil.NewSandbox() + defer sandbox.Close() + + envPath, _ := sandbox.Write("file.env", ` + wrong entry + `) + + args := []string{ + "--use-env-file", + "--env-file", envPath, + } + + data := &struct{}{} + flagParser := flags.NewParser(data, flags.Default) + parser := NewCLIParser(flagParser, "server", func() {}) + + // Act + hookDirSettings, hookSettings, isHelp, err := parser.Parse(args) + + // Assert + require.Error(t, err) + require.False(t, isHelp) + require.Empty(t, hookSettings) + require.Nil(t, hookDirSettings) +} + +// Test that the CLI arguments take precedence over the environment file and +// that the environment file has higher order than the environment variables. +func TestParseArgsFromMultipleSources(t *testing.T) { + // Arrange + // Environment variables - the lowest priority. + restore := testutil.ClearEnvironmentVariables() + defer restore() + + os.Setenv("STORK_DATABASE_HOST", "database-host-envvar") + os.Setenv("STORK_REST_HOST", "rest-host-envvar") + os.Setenv("STORK_REST_TLS_CERTIFICATE", "certificate-envvar") + + // Environment file. Takes precedence over the environment variables. + environmentFile, _ := os.CreateTemp("", "stork-envfile-test-*") + defer func() { + environmentFile.Close() + os.Remove(environmentFile.Name()) + }() + environmentFile.WriteString("STORK_REST_HOST=rest-host-envfile\n") + environmentFile.WriteString("STORK_REST_TLS_CERTIFICATE=certificate-envfile\n") + + // CLI arguments - the highest priority. + args := []string{ + "--rest-tls-certificate", "certificate-cli", + "--use-env-file", + "--env-file", environmentFile.Name(), + } + + type settings struct { + DBHost string `long:"db-host" description:"The host name, IP address or socket where database is available" env:"STORK_DATABASE_HOST" default:""` + RESTHost string `long:"rest-host" description:"The IP to listen on" default:"" env:"STORK_REST_HOST"` + TLSCert string `long:"rest-tls-certificate" description:"The path to the TLS certificate" env:"STORK_REST_TLS_CERTIFICATE" default:""` + } + + data := &settings{} + + parser := NewCLIParser(flags.NewParser(data, flags.Default), "server", func() {}) + // Act + hookDirSettings, hookSettings, isHelp, err := parser.Parse(args) + + // Assert + require.NoError(t, err) + require.False(t, isHelp) + require.Empty(t, hookSettings) + require.NotNil(t, hookDirSettings) + require.Equal(t, "/usr/lib/stork-server/hooks", hookDirSettings.HookDirectory) + require.EqualValues(t, "database-host-envvar", data.DBHost) + require.EqualValues(t, "rest-host-envfile", data.RESTHost) + require.EqualValues(t, "certificate-cli", data.TLSCert) +} + +// Test that the parser throws an error if the arguments are wrong. +func TestCLIParserRejectsWrongCLIArguments(t *testing.T) { + // Arrange + args := []string{"--foo-bar-baz"} + + type settings struct { + DBHost string `long:"db-host" description:"The host name, IP address or socket where database is available" env:"STORK_DATABASE_HOST" default:""` + } + + data := &settings{} + parser := NewCLIParser(flags.NewParser(data, flags.Default), "server", func() {}) + + // Act + hookDirSettings, hookSettings, isHelp, err := parser.Parse(args) + + // Assert + require.Error(t, err) + require.False(t, isHelp) + require.Empty(t, hookSettings) + require.Nil(t, hookDirSettings) +} + +// Test that the namespaces are correct. +func TestHookNamespaces(t *testing.T) { + // Arrange + hookNames := []string{ + "foo", + "foo-bar", + "foo_bar", + "foo-42", + "foo-!@#", + "foo bar", + "foo.bar", + "FOO", + "fOo", + "FoO", + "stork-server-foo", + } + expectedFlagNamespaces := []string{ + "foo", + "foo-bar", + "foo-bar", + "foo-42", + "foo-!@#", + "foo-bar", + "foo-bar", + "foo", + "foo", + "foo", + "foo", + } + expectedEnvironmentNamespaces := []string{ + "STORK_SERVER_HOOK_FOO", + "STORK_SERVER_HOOK_FOO_BAR", + "STORK_SERVER_HOOK_FOO_BAR", + "STORK_SERVER_HOOK_FOO_42", + "STORK_SERVER_HOOK_FOO_!@#", + "STORK_SERVER_HOOK_FOO_BAR", + "STORK_SERVER_HOOK_FOO_BAR", + "STORK_SERVER_HOOK_FOO", + "STORK_SERVER_HOOK_FOO", + "STORK_SERVER_HOOK_FOO", + "STORK_SERVER_HOOK_FOO", + } + + for i := 0; i < len(hookNames); i++ { + hookName := hookNames[i] + t.Run(hookName, func(t *testing.T) { + // Act + flagNamespace, envNamespace := getHookNamespaces("server", hookName) + // Assert + require.Equal(t, expectedFlagNamespaces[i], flagNamespace) + require.Equal(t, expectedEnvironmentNamespaces[i], envNamespace) + }) + } +} + +// Test that the error is returned if the hook directory path points to a file. +func TestCollectHookCLIFlagsForNonDirectoryPath(t *testing.T) { + // Arrange + sandbox := testutil.NewSandbox() + defer sandbox.Close() + path, _ := sandbox.Join("file.ext") + + type settings struct{} + data := &settings{} + + parser := NewCLIParser(flags.NewParser(data, flags.Default), "server", func() {}) + + // Act + args := []string{"--hook-directory", path} + hookDirSettings, hookFlags, isHelp, err := parser.Parse(args) + + // Assert + require.ErrorContains(t, err, "hook directory path is not pointing to a directory") + require.False(t, isHelp) + require.Empty(t, hookFlags) + require.Nil(t, hookDirSettings) +} + +// Test that the no error is returned if the hook directory doesn't exist. +func TestCollectHookCLIFlagsForMissingDirectory(t *testing.T) { + // Arrange + sb := testutil.NewSandbox() + defer sb.Close() + parser := NewCLIParser(nil, "server", func() {}) + hookSettings := &HookDirectorySettings{ + path.Join(sb.BasePath, "non-exists-directory"), + } + + // Act + flags, err := parser.collectHookCLIFlags(hookSettings) + + // Assert + require.NoError(t, err) + require.NotNil(t, flags) + require.Empty(t, flags) +} + +// Test that the hook settings are properly parsed from environment variables. +func TestParseHookSettingsFromEnvironmentVariables(t *testing.T) { + // Arrange + restore := testutil.ClearEnvironmentVariables() + defer restore() + os.Setenv("STORK_SERVER_HOOK_BAZ_FOO_BAR", "fooBar") + + args := []string{} + + type hookSettings struct { + FooBar string `long:"foo-bar" env:"FOO_BAR"` + } + + hookFlags := map[string]hooks.HookSettings{ + "baz": &hookSettings{}, + } + + data := &struct{}{} + parser := NewCLIParser(flags.NewParser(data, flags.Default), "server", func() {}) + + // Act + mergeErr := parser.mergeHookFlags(hookFlags) + parseErr := parser.parse(args) + + // Assert + require.NoError(t, mergeErr) + require.NoError(t, parseErr) + + // ToDO: Fix this test + // require.Contains(t, settings.HooksSettings, "baz") + require.Equal(t, "fooBar", hookFlags["baz"].(*hookSettings).FooBar) +} + +// Test that the hook settings are properly parsed from the CLI arguments. +func TestParseHookSettingsFromCLI(t *testing.T) { + // Arrange + args := []string{"--baz.foo-bar", "fooBar"} + + type hookSettings struct { + FooBar string `long:"foo-bar" env:"FOO_BAR"` + } + + hookFlags := map[string]hooks.HookSettings{ + "baz": &hookSettings{}, + } + + data := &struct{}{} + parser := NewCLIParser(flags.NewParser(data, flags.Default), "server", func() {}) + + // Act + mergeErr := parser.mergeHookFlags(hookFlags) + parseErr := parser.parse(args) + + // Assert + require.NoError(t, mergeErr) + require.NoError(t, parseErr) + // TODO: Fix this test + // require.Contains(t, settings.HooksSettings, "baz") + require.Equal(t, "fooBar", hookFlags["baz"].(*hookSettings).FooBar) +} + +// Test that an error is returned if the two hooks are solved to the same +// namespace. +func TestPaseHookSettingsDuplicatedNamespace(t *testing.T) { + // Arrange + os.Args = []string{ + "program-name", + "--baz.foo-bar", "fooBar", + } + + type hookSettings struct { + FooBar string `long:"foo-bar" env:"FOO_BAR"` + } + + hookFlags := map[string]hooks.HookSettings{ + "baz": &hookSettings{}, + "stork-server-baz": &hookSettings{}, + } + + data := &struct{}{} + parser := NewCLIParser(flags.NewParser(data, flags.Default), "server", func() {}) + + // Act + err := parser.mergeHookFlags(hookFlags) + + // Assert + require.ErrorContains(t, err, "two hooks using the same configuration namespace") +} + +// Test that the help is properly printed and it includes the hook settings. +func TestParseHelp(t *testing.T) { + // Arrange + args := []string{"--help"} + + type hookSettings struct { + FooBar string `long:"foo-bar" description:"Lorem ipsum" env:"FOO_BAR"` + } + + hookFlags := map[string]hooks.HookSettings{ + "baz": &hookSettings{}, + } + + type settings struct { + TLSCert string `long:"tls-cert" env:"TLS_CERT" description:"The path to the TLS certificate"` + } + data := &settings{} + + coreParser := flags.NewParser(data, flags.Default) + coreParser.Name = "program-name" + + parser := NewCLIParser(coreParser, "server", func() {}) + _ = parser.mergeHookFlags(hookFlags) + + // Act + var isHelp bool + var err error + stdout, stderr, captureErr := testutil.CaptureOutput(func() { + _, _, isHelp, err = parser.Parse(args) + }) + + // Assert + require.NoError(t, err) + require.NoError(t, captureErr) + require.True(t, isHelp) + require.Empty(t, stderr) + + expectedHelp := `Usage: + program-name [OPTIONS] + +Application Options: + --tls-cert= The path to the TLS certificate [$TLS_CERT] + +Hook 'baz' Flags: + --baz.foo-bar= Lorem ipsum [$STORK_SERVER_HOOK_BAZ_FOO_BAR] + +Hook Directory Flags: + --hook-directory= The path to the hook directory (default: + /usr/lib/stork-server/hooks) + [$STORK_SERVER_HOOK_DIRECTORY] + +Environment File Flags: + --env-file= Environment file location; applicable only if the + use-env-file is provided (default: + /etc/stork/server.env) + --use-env-file Read the environment variables from the environment file + +Help Options: + -h, --help Show this help message + +` + + require.Equal(t, expectedHelp, string(stdout)) +} + +// Test that the unknown environment variables from the environment file are +// logged and ignored. +func TestVerifyEnvironmentFile(t *testing.T) { + // Arrange + restore := testutil.ClearEnvironmentVariables() + defer restore() + + sandbox := testutil.NewSandbox() + defer sandbox.Close() + + envPath, _ := sandbox.Write("file.env", ` +STORK_SERVER_TLS_CERT=tlsCert +STORK_SERVER_UNKNOWN=unknown +FOOBAR=foobar +`) + + type settings struct { + TLSCert string `long:"tls-cert" env:"STORK_SERVER_TLS_CERT" description:"The path to the TLS certificate"` + } + data := &settings{} + + parser := NewCLIParser(flags.NewParser(data, flags.Default), "server", func() {}) + + // Act + var err error + stdout, _, captureErr := testutil.CaptureOutput(func() { + err = parser.verifyEnvironmentFile(&environmentFileSettings{ + EnvFile: envPath, + UseEnvFile: true, + }) + }) + + // Assert + require.NoError(t, err) + require.NoError(t, captureErr) + + expectedLog := fmt.Sprintf( + `Unknown environment variable: 'STORK_SERVER_UNKNOWN' in the environment file: '%s'`, + envPath, + ) + require.Contains(t, string(stdout), expectedLog) + require.NotContains(t, string(stdout), "TLS_CERT") + require.Contains(t, string(stdout), "Unknown environment variable: 'FOOBAR'") +} + +// Test that the unknown environment variables from the environment file are +// logged and ignored even if the parser has subcommands. +func TestVerifyEnvironmentFileForParserWithSubcommands(t *testing.T) { + // Arrange + restore := testutil.ClearEnvironmentVariables() + defer restore() + + sandbox := testutil.NewSandbox() + defer sandbox.Close() + + envPath, _ := sandbox.Write("file.env", ` +STORK_SERVER_TLS_CERT=tlsCert +STORK_DATABASE_HOST=databaseHost +STORK_SERVER_UNKNOWN=unknown +FOOBAR=foobar +`) + + type settings struct { + TLSCert string `long:"tls-cert" env:"STORK_SERVER_TLS_CERT" description:"The path to the TLS certificate"` + } + + type subcommandSettings struct { + DBHost string `long:"db-host" env:"STORK_DATABASE_HOST" description:"The host name, IP address or socket where database is available"` + } + + data := &settings{} + subcommandData := &subcommandSettings{} + + coreParser := flags.NewParser(data, flags.Default) + coreParser.AddCommand("subcommand", "Subcommand", "", subcommandData) + + parser := NewCLIParser(coreParser, "server", func() {}) + + // Act + var err error + stdout, _, captureErr := testutil.CaptureOutput(func() { + err = parser.verifyEnvironmentFile(&environmentFileSettings{ + EnvFile: envPath, + UseEnvFile: true, + }) + }) + + // Assert + require.NoError(t, err) + require.NoError(t, captureErr) + + expectedLog := fmt.Sprintf( + `Unknown environment variable: 'STORK_SERVER_UNKNOWN' in the environment file: '%s'`, + envPath, + ) + require.Contains(t, string(stdout), expectedLog) + require.NotContains(t, string(stdout), "TLS_CERT") + require.NotContains(t, string(stdout), "DATABASE_HOST") + require.Contains(t, string(stdout), "Unknown environment variable: 'FOOBAR'") +} + +// Test that the unknown system-wide environment variables are logged and +// ignored. +func TestVerifySystemEnvironmentVariables(t *testing.T) { + // Arrange + restore := testutil.ClearEnvironmentVariables() + defer restore() + + type settings struct { + TLSCert string `long:"tls-cert" env:"STORK_SERVER_TLS_CERT" description:"The path to the TLS certificate"` + DBHost string `long:"db-host" env:"STORK_DATABASE_HOST" description:"The host name, IP address or socket where database is available"` + } + data := &settings{} + + parser := NewCLIParser(flags.NewParser(data, flags.Default), "server", func() {}) + + os.Setenv("STORK_SERVER_UNKNOWN", "unknown") + os.Setenv("STORK_SERVER_TLS_CERT", "tlsCert") + os.Setenv("STORK_DATABASE_HOST", "databaseHost") + os.Setenv("STORK_DATABASE_UNKNOWN", "databaseUnknown") + os.Setenv("FOOBAR", "foobar") + // Broken naming convention. + os.Setenv("STORK_UNKNOWN", "unknown") + + // Act + stdout, _, captureErr := testutil.CaptureOutput(func() { + parser.verifySystemEnvironmentVariables() + }) + + // Assert + require.NoError(t, captureErr) + + expectedLog := `Unknown environment variable: 'STORK_SERVER_UNKNOWN' set in a shell` + require.Contains(t, string(stdout), expectedLog) + require.Contains(t, string(stdout), "Unknown environment variable: 'STORK_DATABASE_UNKNOWN' set in a shell") + require.NotContains(t, string(stdout), "TLS_CERT") + require.NotContains(t, string(stdout), "FOOBAR") + require.NotContains(t, string(stdout), "DATABASE_HOST") + require.NotContains(t, string(stdout), "STORK_UNKNOWN") +} + +// Test that the environment variables from another Stork application set in +// the shell don't raise a warning. +func TestVerifySystemEnvironmentVariablesFromAnotherApplication(t *testing.T) { + // Arrange + restore := testutil.ClearEnvironmentVariables() + defer restore() + + type settings struct { + TLSCert string `long:"tls-cert" env:"STORK_SERVER_TLS_CERT" description:"The path to the TLS certificate"` + } + data := &settings{} + + parser := NewCLIParser(flags.NewParser(data, flags.Default), "server", func() {}) + + // Act + os.Setenv("STORK_AGENT_TLS_CERT", "tlsCert") + + stdout, _, captureErr := testutil.CaptureOutput(func() { + parser.verifySystemEnvironmentVariables() + }) + + // Assert + require.NoError(t, captureErr) + require.NotContains(t, string(stdout), "TLS_CERT") +} + +// Test that the callback is called when the environment file is loaded. +func TestOnEnvironmentFileLoadedCallbackIsCalled(t *testing.T) { + // Arrange + restore := testutil.ClearEnvironmentVariables() + defer restore() + + sandbox := testutil.NewSandbox() + defer sandbox.Close() + + envPath, _ := sandbox.Write("file.env", ` + STORK_SERVER_TLS_CERT=tlsCert + `) + + isCalled := false + callback := func() { + isCalled = true + } + + parser := NewCLIParser(flags.NewParser(&struct{}{}, flags.Default), "server", callback) + + // Act + err := parser.loadEnvironmentFile(&environmentFileSettings{ + EnvFile: envPath, + UseEnvFile: true, + }) + + // Assert + require.NoError(t, err) + require.True(t, isCalled) +} + +// Test the callback is called when the environment file is loaded exactly once. +func TestOnEnvironmentFileLoadedCallbackIsCalledOnce(t *testing.T) { + // Arrange + restorePoint := testutil.ClearEnvironmentVariables() + defer restorePoint() + sandbox := testutil.NewSandbox() + defer sandbox.Close() + + envPath, _ := sandbox.Write("file.env", ` + STORK_DATABASE_HOST=foo + STORK_SERVER_HOOK_DIRECTORY=bar + STORK_REST_HOST=baz + `) + + args := []string{ + "--use-env-file", + "--env-file", envPath, + } + + type settings struct { + DBHost string `long:"db-host" description:"The host name, IP address or socket where database is available" env:"STORK_DATABASE_HOST" default:""` + RESTHost string `long:"rest-host" description:"The IP to listen on" default:"" env:"STORK_REST_HOST"` + } + + data := &settings{} + + flagParser := flags.NewParser(data, flags.Default) + + parser := NewCLIParser(flagParser, "server", func() {}) + + // Act + hookDirSettings, hookFlags, isHelp, err := parser.Parse(args) + + // Assert + require.NoError(t, err) + require.False(t, isHelp) + require.Empty(t, hookFlags) + + require.Equal(t, "foo", data.DBHost) + require.Equal(t, "bar", hookDirSettings.HookDirectory) + require.Equal(t, "baz", data.RESTHost) +} + +// Test the hook directory is enabled only for the server and agent applications. +func TestHookDirectoryDependsOnApplication(t *testing.T) { + // Arrange + defer testutil.ClearEnvironmentVariables()() + sandbox := testutil.NewSandbox() + defer sandbox.Close() + + args := []string{ + "--hook-directory", sandbox.BasePath, + } + + data := &struct{}{} + + t.Run("server", func(t *testing.T) { + parser := NewCLIParser( + flags.NewParser(data, flags.Default), + "server", func() {}, + ) + + // Act + hookDirSettings, hookFlags, isHelp, err := parser.Parse(args) + + // Assert + require.NoError(t, err) + require.False(t, isHelp) + require.NotNil(t, hookDirSettings) + require.NotNil(t, hookFlags) + }) + + t.Run("agent", func(t *testing.T) { + parser := NewCLIParser( + flags.NewParser(data, flags.Default), + "agent", func() {}, + ) + + // Act + hookDirSettings, hookFlags, isHelp, err := parser.Parse(args) + + // Assert + require.NoError(t, err) + require.False(t, isHelp) + require.NotNil(t, hookDirSettings) + require.NotNil(t, hookFlags) + }) + + t.Run("tool", func(t *testing.T) { + parser := NewCLIParser( + flags.NewParser(data, flags.Default), + "tool", func() {}, + ) + + // Act + hookDirSettings, hookFlags, isHelp, err := parser.Parse(args) + + // Assert + require.ErrorContains(t, err, "unknown flag `hook-directory'") + require.False(t, isHelp) + require.Nil(t, hookDirSettings) + require.Nil(t, hookFlags) + }) + + t.Run("code-gen", func(t *testing.T) { + parser := NewCLIParser(flags.NewParser(data, flags.Default), "code-gen", func() {}) + + // Act + hookDirSettings, hookFlags, isHelp, err := parser.Parse(args) + + // Assert + require.ErrorContains(t, err, "unknown flag `hook-directory'") + require.False(t, isHelp) + require.Nil(t, hookDirSettings) + require.Nil(t, hookFlags) + }) +} diff --git a/backend/cmd/stork-agent/main.go b/backend/cmd/stork-agent/main.go index 7ebd5f61c..f52a14f1a 100644 --- a/backend/cmd/stork-agent/main.go +++ b/backend/cmd/stork-agent/main.go @@ -16,6 +16,7 @@ import ( log "github.com/sirupsen/logrus" "isc.org/stork" "isc.org/stork/agent" + cli "isc.org/stork/cli" "isc.org/stork/hooks" "isc.org/stork/profiler" storkutil "isc.org/stork/util" @@ -315,15 +316,8 @@ func runRegister(settings *registerSettings) { } } -// Read environment file settings. It's parsed before the main settings. -type environmentFileSettings struct { - EnvFile string `long:"env-file" description:"Environment file location; applicable only if the use-env-file is provided" default:"/etc/stork/agent.env"` - UseEnvFile bool `long:"use-env-file" description:"Read the environment variables from the environment file"` -} - // General Stork Agent settings. They are used when no command is specified. type generalSettings struct { - environmentFileSettings Version bool `short:"v" long:"version" description:"Show software version"` Host string `long:"host" description:"The IP or hostname to listen on for incoming Stork Server connections" default:"0.0.0.0" env:"STORK_AGENT_HOST"` Port int `long:"port" description:"The TCP port to listen on for incoming Stork Server connections" default:"8080" env:"STORK_AGENT_PORT"` @@ -336,13 +330,12 @@ type generalSettings struct { PrometheusBind9ExporterPort int `long:"prometheus-bind9-exporter-port" description:"The port to listen on for incoming Prometheus connections" default:"9119" env:"STORK_AGENT_PROMETHEUS_BIND9_EXPORTER_PORT"` SkipTLSCertVerification bool `long:"skip-tls-cert-verification" description:"Skip TLS certificate verification when the Stork Agent makes HTTP calls over TLS" env:"STORK_AGENT_SKIP_TLS_CERT_VERIFICATION"` ServerURL string `long:"server-url" description:"The URL of the Stork Server, used in agent-token-based registration (optional alternative to server-token-based registration)" env:"STORK_AGENT_SERVER_URL"` - HookDirectory string `long:"hook-directory" description:"The path to the hook directory" default:"/usr/lib/stork-agent/hooks" env:"STORK_AGENT_HOOK_DIRECTORY"` Bind9Path string `long:"bind9-path" description:"Specify the path to BIND 9 config file. Does not need to be specified, unless the location is very uncommon." env:"STORK_AGENT_BIND9_CONFIG"` + HookDirectory string } // Register command settings. type registerSettings struct { - environmentFileSettings // It is true if the register command was specified. Otherwise, it is false. commandSpecified bool NonInteractive bool `short:"n" long:"non-interactive" description:"Do not prompt for missing arguments" env:"STORK_AGENT_NON_INTERACTIVE"` @@ -409,48 +402,19 @@ func (s *registerSettings) Execute(_ []string) error { // Parses the command line arguments. Returns the general settings if no command // is specified, the register settings if the register command is specified, -// or an error if the arguments are invalid, the command is unknown, or the -// help is requested. -func parseArgs() (*generalSettings, *registerSettings, error) { - shortGeneralDescription := "Stork Agent" - longGeneralDescription := `This component is required on each machine to be monitored by the Stork Server - -Stork logs at INFO level by default. Other levels can be configured using the -STORK_LOG_LEVEL variable. Allowed values are: DEBUG, INFO, WARN, ERROR.` - - // Parse environment file settings. - envFileSettings := &environmentFileSettings{} - parser := flags.NewParser(envFileSettings, flags.IgnoreUnknown) - parser.ShortDescription = shortGeneralDescription - parser.LongDescription = longGeneralDescription - - if _, err := parser.Parse(); err != nil { - err = errors.Wrap(err, "invalid CLI argument") - return nil, nil, err - } - - // Load environment variables from the environment file. - if envFileSettings.UseEnvFile { - err := storkutil.LoadEnvironmentFileToSetter( - envFileSettings.EnvFile, - storkutil.NewProcessEnvironmentVariableSetter(), - ) - if err != nil { - err = errors.WithMessagef(err, "invalid environment file: '%s'", envFileSettings.EnvFile) - return nil, nil, err - } - - // Reconfigures logging using new environment variables. - storkutil.SetupLogging() - } - +// a flag indicating if the help is requested, or an error if the arguments are +// invalid, the command is unknown, or the help is requested. +func parseArgs() (*generalSettings, *registerSettings, bool, error) { // Prepare main parser. generalSettings := &generalSettings{} registerSettings := ®isterSettings{} - parser = flags.NewParser(generalSettings, flags.Default) - parser.ShortDescription = shortGeneralDescription - parser.LongDescription = longGeneralDescription + parser := flags.NewParser(generalSettings, flags.Default) + parser.ShortDescription = "Stork Agent" + parser.LongDescription = `This component is required on each machine to be monitored by the Stork Server + +Stork logs at INFO level by default. Other levels can be configured using the +STORK_LOG_LEVEL variable. Allowed values are: DEBUG, INFO, WARN, ERROR.` parser.SubcommandsOptional = true _, err := parser.AddCommand( @@ -465,15 +429,22 @@ authorization in the server using either the UI or the ReST API (agent-token-bas ) if err != nil { err = errors.Wrap(err, "invalid CLI 'register' command") - return nil, nil, err + return nil, nil, false, err } // Parse command line arguments. - _, err = parser.Parse() + appParser := cli.NewCLIParser(parser, "agent", func() { + storkutil.SetupLogging() + }) + + hookDirectorySettings, _, isHelp, err := appParser.Parse(os.Args[1:]) if err != nil { err = errors.Wrap(err, "invalid CLI argument") - return nil, nil, err + return nil, nil, false, err + } else if isHelp { + return nil, nil, true, nil } + generalSettings.HookDirectory = hookDirectorySettings.HookDirectory if registerSettings.commandSpecified { generalSettings = nil @@ -481,18 +452,7 @@ authorization in the server using either the UI or the ReST API (agent-token-bas registerSettings = nil } - return generalSettings, registerSettings, nil -} - -// Check if a given error is a request to display the help. -func isHelpRequest(err error) bool { - var flagsError *flags.Error - if errors.As(err, &flagsError) { - if flagsError.Type == flags.ErrHelp { - return true - } - } - return false + return generalSettings, registerSettings, false, nil } // Parses the command line arguments and runs the specific Stork Agent command. @@ -500,11 +460,11 @@ func runApp(reload bool) error { profilerShutdown := profiler.Start(profiler.AgentProfilerPort) defer profilerShutdown() - generalSettings, registerSettings, err := parseArgs() + generalSettings, registerSettings, isHelp, err := parseArgs() + if isHelp { + return nil + } if err != nil { - if isHelpRequest(err) { - return nil - } return err } diff --git a/backend/cmd/stork-agent/main_test.go b/backend/cmd/stork-agent/main_test.go index 686aca0dd..28ad6f4cb 100644 --- a/backend/cmd/stork-agent/main_test.go +++ b/backend/cmd/stork-agent/main_test.go @@ -148,7 +148,7 @@ func TestRegistrationParams(t *testing.T) { func TestRegistrationParamsFromEnvironmentFile(t *testing.T) { // Arrange defer testutil.CreateOsArgsRestorePoint()() - defer testutil.CreateEnvironmentRestorePoint()() + defer testutil.ClearEnvironmentVariables()() sandbox := testutil.NewSandbox() defer sandbox.Close() diff --git a/backend/cmd/stork-code-gen/main.go b/backend/cmd/stork-code-gen/main.go index 5ded39dd5..6b77e5158 100644 --- a/backend/cmd/stork-code-gen/main.go +++ b/backend/cmd/stork-code-gen/main.go @@ -4,59 +4,53 @@ import ( "fmt" "os" - "github.com/urfave/cli/v2" - "isc.org/stork" + "github.com/jessevdk/go-flags" + "github.com/sirupsen/logrus" + "isc.org/stork/cli" "isc.org/stork/codegen" ) +type stdOptionDefinitionsSettings struct { + cli.CommandSettings + Input string `short:"i" long:"input" description:"Path to the input file holding option definitions' specification." required:"true"` + Output string `short:"o" long:"output" description:"Path to the output file or 'stdout' to print the generated code in the terminal." required:"true"` + Template string `short:"t" long:"template" description:"Path to the template file used to generate the output file. The generated code is embedded in the template file."` +} + // Generates the code defining standard option definitions to stdout or // to a file. -func generateStdOptionDefs(c *cli.Context) error { +func generateStdOptionDefs(settings *stdOptionDefinitionsSettings) error { // Print the output to the stdout or to a file. - if c.String("output") == "stdout" { - return codegen.GenerateToStdout(c.String("input"), c.String("template")) + if settings.Output == "stdout" { + return codegen.GenerateToStdout(settings.Input, settings.Template) } - return codegen.GenerateToFile(c.String("input"), c.String("template"), c.String("output")) + return codegen.GenerateToFile(settings.Input, settings.Template, settings.Output) } // Man function exposing command line parameters. func main() { - app := &cli.App{ - Name: "Stork Code Gen", - Usage: "Code generator used in Stork development", - Version: stork.Version, - HelpName: "stork-code-gen", - Flags: []cli.Flag{}, - Commands: []*cli.Command{ - { - Name: "std-option-defs", - Usage: "Generate standard option definitions from JSON spec.", - UsageText: "stork-code-gen std-option-defs", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "input", - Usage: "Path to the input file holding option definitions' specification.", - Required: true, - Aliases: []string{"i"}, - }, - &cli.StringFlag{ - Name: "output", - Usage: "Path to the output file or 'stdout' to print the generated code in the terminal.", - Required: true, - Aliases: []string{"o"}, - }, - &cli.StringFlag{ - Name: "template", - Usage: "Path to the template file used to generate the output file. The generated code is embedded in the template file.", - Aliases: []string{"t"}, - }, - }, - Action: generateStdOptionDefs, - }, + nothing := struct{}{} + parser := flags.NewParser(¬hing, flags.Default) + parser.Name = "stork-code-gen" + parser.ShortDescription = "Code generator used in Stork development" + parser.Usage = "stork-code-gen [command] [options]" + + app := cli.NewApp(parser) + + stdOptionDefinitionsSettings := &stdOptionDefinitionsSettings{} + app.RegisterCommand( + "std-option-defs", + "Generate standard option definitions from JSON spec.", + stdOptionDefinitionsSettings, + func() { + err := generateStdOptionDefs(stdOptionDefinitionsSettings) + if err != nil { + logrus.WithError(err).Fatal("Failed to generate standard option definitions.") + } }, - } + ) - err := app.Run(os.Args) + err := app.Run("code-gen", os.Args[1:]) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/backend/cmd/stork-code-gen/main_test.go b/backend/cmd/stork-code-gen/main_test.go index 97489c45a..b52a1144c 100644 --- a/backend/cmd/stork-code-gen/main_test.go +++ b/backend/cmd/stork-code-gen/main_test.go @@ -27,8 +27,8 @@ func TestMainHelp(t *testing.T) { func TestStdOptionDefsHelp(t *testing.T) { defer testutil.CreateOsArgsRestorePoint()() os.Args = make([]string, 3) - os.Args[1] = "help" - os.Args[2] = "std-option-defs" + os.Args[1] = "std-option-defs" + os.Args[2] = "--help" stdoutBytes, _, err := testutil.CaptureOutput(main) require.NoError(t, err) diff --git a/backend/cmd/stork-tool/main.go b/backend/cmd/stork-tool/main.go index aa90af1b5..6851a1dad 100644 --- a/backend/cmd/stork-tool/main.go +++ b/backend/cmd/stork-tool/main.go @@ -2,19 +2,16 @@ package main import ( "bufio" - "fmt" "io" "io/fs" "os" "path" - "reflect" - "strconv" + "github.com/jessevdk/go-flags" "github.com/pkg/errors" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" - "isc.org/stork" + "isc.org/stork/cli" "isc.org/stork/hooksutil" "isc.org/stork/server/certs" dbops "isc.org/stork/server/database" @@ -24,11 +21,64 @@ import ( // Random hash size in the generated password. const passwordGenRandomLength = 24 +// The CLI flags for the db-create command. +type databaseCreateSettings struct { + cli.CommandSettings + DatabaseSettings dbops.DatabaseCLIFlagsWithMaintenance + Force bool `long:"force" short:"f" description:"Recreate the database and the user if they exist" env:"STORK_TOOL_DB_FORCE"` +} + +// The CLI flags for the db-init, db-up, db-down, db-reset, db-version commands. +type databaseSettings struct { + cli.CommandSettings + DatabaseSettings dbops.DatabaseCLIFlags +} + +// The CLI flags for the db-up, db-down, and db-set-version commands. +type databaseVersionSettings struct { + cli.CommandSettings + DatabaseSettings dbops.DatabaseCLIFlags + Version string `long:"version" short:"t" description:"Target database schema version (optional)" env:"STORK_TOOL_DB_VERSION"` +} + +// The CLI flags for the cert-import command. +type certificateImportSettings struct { + cli.CommandSettings + DatabaseSettings dbops.DatabaseCLIFlags + Object string `long:"object" short:"f" description:"The object to import; it can be one of 'cakey', 'cacert', 'srvkey', 'srvcert', 'srvtkn'" env:"STORK_TOOL_CERT_OBJECT" choice:"cakey" choice:"cacert" choice:"srvkey" choice:"srvcert" choice:"srvtkn"` + File string `long:"file" short:"i" description:"The file location from which the object should be imported" env:"STORK_TOOL_CERT_FILE"` +} + +// The CLI flags for the cert-export command. +type certificateExportSettings struct { + cli.CommandSettings + DatabaseSettings dbops.DatabaseCLIFlags + Object string `long:"object" short:"f" description:"The object to dump; it can be one of 'cakey', 'cacert', 'srvkey', 'srvcert', 'srvtkn'" env:"STORK_TOOL_CERT_OBJECT" choice:"cakey" choice:"cacert" choice:"srvkey" choice:"srvcert" choice:"srvtkn"` + File string `long:"file" short:"o" description:"The file location where the object should be saved; if not provided, then object is printed to stdout" env:"STORK_TOOL_CERT_FILE"` +} + +// The CLI flags for the hook-inspect command. +type hookInspectSettings struct { + cli.CommandSettings + HookPath string `long:"hook-path" short:"p" description:"The path to the hook file or directory" env:"STORK_TOOL_HOOK_PATH"` +} + +// The CLI flags for the deploy-login-page-welcome command. +type loginScreenWelcomeDeploySettings struct { + cli.CommandSettings + File string `long:"file" short:"i" description:"HTML source file with a custom welcome message" env:"STORK_TOOL_LOGIN_SCREEN_WELCOME_FILE"` + RestStaticFilesDir string `long:"rest-static-files-dir" short:"d" description:"The directory with static files for the UI; if not provided the tool will try to use default locations" env:"STORK_TOOL_REST_STATIC_FILES_DIR"` +} + +// The CLI flags for the undeploy-login-page-welcome command. +type loginScreenWelcomeUndeploySettings struct { + cli.CommandSettings + RestStaticFilesDir string `long:"rest-static-files-dir" short:"d" description:"The directory with static files for the UI; if not provided the tool will try to use default locations" env:"STORK_TOOL_REST_STATIC_FILES_DIR"` +} + // Establish connection to a database with opts from command line. // Returns the database instance. It must be closed by caller. -func getDBConn(rawFlags *cli.Context) *dbops.PgDB { - flags := &dbops.DatabaseCLIFlags{} - flags.ReadFromCLI(rawFlags) +func getDBConn(flags dbops.DatabaseCLIFlags) *dbops.PgDB { settings, err := flags.ConvertToDatabaseSettings() if err != nil { log.WithError(err).Fatal("Invalid database settings") @@ -50,21 +100,18 @@ func getDBConn(rawFlags *cli.Context) *dbops.PgDB { // Execute db-create command. It prepares new database for the Stork // server. It also creates a user that can access this database using // a generated or user-specified password and the pgcrypto extension. -func runDBCreate(context *cli.Context) { - flags := &dbops.DatabaseCLIFlagsWithMaintenance{} - flags.ReadFromCLI(context) - +func runDBCreate(command *databaseCreateSettings) { var err error // Prepare logging fields. logFields := log.Fields{ - "database_name": flags.DBName, - "user": flags.User, + "database_name": command.DatabaseSettings.DBName, + "user": command.DatabaseSettings.User, } // Check if the password has been specified explicitly. Otherwise, // generate the password. - password := flags.Password + password := command.DatabaseSettings.Password if len(password) == 0 { password, err = storkutil.Base64Random(passwordGenRandomLength) if err != nil { @@ -73,11 +120,11 @@ func runDBCreate(context *cli.Context) { // Only log the password if it has been generated. Otherwise, the // user should know the password. logFields["password"] = password - flags.Password = password + command.DatabaseSettings.Password = password } // Connect to the postgres database using admin credentials. - settings, err := flags.ConvertToMaintenanceDatabaseSettings() + maintenanceSettings, err := command.DatabaseSettings.ConvertToMaintenanceDatabaseSettings() if err != nil { log.WithError(err).Fatal("Invalid database settings") } @@ -85,11 +132,11 @@ func runDBCreate(context *cli.Context) { // Try to create the database and the user with access using // specified password. err = dbops.CreateDatabase( - *settings, - flags.DBName, - flags.User, - flags.Password, - context.Bool("force"), + *maintenanceSettings, + command.DatabaseSettings.DBName, + command.DatabaseSettings.User, + command.DatabaseSettings.Password, + command.Force, ) if err != nil { log.WithError(err).Fatal("Could not create the database and the user") @@ -112,7 +159,7 @@ func runDBPasswordGen() { } // Execute DB migration command. -func runDBMigrate(settings *cli.Context, command, version string) { +func runDBMigrate(databaseSettings dbops.DatabaseCLIFlags, command, version string) { // The up and down commands require special treatment. If the target version is specified // it must be appended to the arguments we pass to the go-pg migrations. var args []string @@ -133,12 +180,12 @@ func runDBMigrate(settings *cli.Context, command, version string) { log.Infof("Requested setting version to %s", version) } - traceSQL := settings.String("db-trace-queries") + traceSQL := databaseSettings.TraceSQL if traceSQL != "" { log.Infof("SQL queries tracing set to %s", traceSQL) } - db := getDBConn(settings) + db := getDBConn(databaseSettings) oldVersion, newVersion, err := dbops.Migrate(db, args...) if err == nil && newVersion == 0 { @@ -167,19 +214,19 @@ func runDBMigrate(settings *cli.Context, command, version string) { } // Execute cert export command. -func runCertExport(settings *cli.Context) error { - db := getDBConn(settings) +func runCertExport(certificateSettings *certificateExportSettings) error { + db := getDBConn(certificateSettings.DatabaseSettings) defer db.Close() - return certs.ExportSecret(db, settings.String("object"), settings.String("file")) + return certs.ExportSecret(db, certificateSettings.Object, certificateSettings.File) } // Execute cert import command. -func runCertImport(settings *cli.Context) error { - db := getDBConn(settings) +func runCertImport(certificateSettings *certificateImportSettings) error { + db := getDBConn(certificateSettings.DatabaseSettings) defer db.Close() - return certs.ImportSecret(db, settings.String("object"), settings.String("file")) + return certs.ImportSecret(db, certificateSettings.Object, certificateSettings.File) } // Inspect the hook file. @@ -205,8 +252,8 @@ func inspectHookFile(path string, library *hooksutil.LibraryManager, err error) } // Execute inspect hook command. -func runHookInspect(settings *cli.Context) error { - hookPath := settings.String("path") +func runHookInspect(hookInspectSettings *hookInspectSettings) error { + hookPath := hookInspectSettings.HookPath fileInfo, err := os.Stat(hookPath) if err != nil { return errors.Wrapf(err, "cannot stat the hook path: '%s'", hookPath) @@ -231,9 +278,9 @@ func runHookInspect(settings *cli.Context) error { } // Deploy specified static file view into assets/static-page-content. -func runStaticViewDeploy(settings *cli.Context, outFilename string) error { +func runStaticViewDeploy(settings *loginScreenWelcomeDeploySettings, outFilename string) error { // Basic checks on the input file. - inFilename := settings.String("file") + inFilename := settings.File if _, err := os.Stat(inFilename); err != nil { switch { case errors.Is(err, fs.ErrNotExist): @@ -249,7 +296,7 @@ func runStaticViewDeploy(settings *cli.Context, outFilename string) error { } } // Get the directory where our file is to be copied. - outDirectory, err := getOrLocateStaticPageContentDir(settings) + outDirectory, err := getOrLocateStaticPageContentDir(settings.RestStaticFilesDir) if err != nil { return err } @@ -282,9 +329,9 @@ func runStaticViewDeploy(settings *cli.Context, outFilename string) error { } // Undeploy specified static file view from assets/static-page-content. -func runStaticViewUndeploy(settings *cli.Context, filename string) error { +func runStaticViewUndeploy(settings *loginScreenWelcomeUndeploySettings, filename string) error { // Get the directory where our file is to be copied. - directory, err := getOrLocateStaticPageContentDir(settings) + directory, err := getOrLocateStaticPageContentDir(settings.RestStaticFilesDir) if err != nil { return err } @@ -300,9 +347,9 @@ func runStaticViewUndeploy(settings *cli.Context, filename string) error { // the path to assets/static-page-content relative to this path. If the // path is not specified it tries to locate the static-page-content path // relative to the stork-tool binary name. -func getOrLocateStaticPageContentDir(settings *cli.Context) (string, error) { +func getOrLocateStaticPageContentDir(restStaticFilesDirectory string) (string, error) { // Get the directory where our file is to be copied. - directory := settings.String("rest-static-files-dir") + directory := restStaticFilesDirectory if directory == "" { // The directory hasn't been specified. Let's try to locate that directory // relative to the stork-tool binary location. @@ -336,355 +383,186 @@ func getOrLocateStaticPageContentDir(settings *cli.Context) (string, error) { return directory, nil } -// Parse the general flag definitions into the objects compatible with the CLI library. -func parseFlagDefinitions(flagDefinitions []*dbops.CLIFlagDefinition) ([]cli.Flag, error) { - var flags []cli.Flag - for _, definition := range flagDefinitions { - var flag cli.Flag +// Prepare CLI app with all flags and commands defined. +func newApp() *cli.App { + nothing := struct{}{} + parser := flags.NewParser(¬hing, flags.Default) - var aliases []string - if definition.Short != "" { - aliases = append(aliases, definition.Short) - } + parser.Name = "stork-tool" + parser.SubcommandsOptional = true + parser.ShortDescription = "A tool for managing Stork Server." + parser.LongDescription = `The tool operates in four areas: - var envVars []string - if definition.EnvironmentVariable != "" { - envVars = append(envVars, definition.EnvironmentVariable) - } - - if definition.Kind == reflect.Int { - valueInt, err := strconv.ParseInt(definition.Default, 10, 0) - if err != nil { - return nil, errors.Wrapf( - err, "invalid default value ('%s') for parameter ('%s')", - definition.Default, definition.Long, - ) - } + - Certificate Management - it allows for exporting Stork Server keys, certificates, + and tokens that are used to secure communication between the Stork Server + and Stork Agents; - flag = &cli.Int64Flag{ - Name: definition.Long, - Aliases: aliases, - Usage: definition.Description, - EnvVars: envVars, - Value: valueInt, - } - } else { - flag = &cli.StringFlag{ - Name: definition.Long, - Aliases: aliases, - Usage: definition.Description, - EnvVars: envVars, - Value: definition.Default, - } - } + - Database Creation - it facilitates creating a new database for the Stork Server, + and a user that can access this database with a generated password; - flags = append(flags, flag) - } + - Database Migration - it allows for performing database schema migrations, + overwriting the db schema version and getting its current value; - return flags, nil -} + - Static Views Deployment - it allows for setting custom content in selected + Stork views (e.g., custom welcome message on the login page).` -// Prepare urfave cli app with all flags and commands defined. -func setupApp() *cli.App { - cli.VersionPrinter = func(c *cli.Context) { - fmt.Println(c.App.Version) - } + app := cli.NewApp(parser) - dbFlags, err := parseFlagDefinitions((*dbops.DatabaseCLIFlags)(nil).ConvertToCLIFlagDefinitions()) - if err != nil { - log.WithError(err).Fatal("Invalid database CLI flag definitions") - } + // Disclaimer: The previous version grouped the commands into separate + // sections. Unfortunately, the go-flags library does not support this + // feature. - dbCreateFlags, err := parseFlagDefinitions((*dbops.DatabaseCLIFlagsWithMaintenance)(nil).ConvertToCLIFlagDefinitions()) - if err != nil { - log.WithError(err).Fatal("Invalid create database CLI flag definitions") - } + // Database creation commands. + databaseCreateSettings := &databaseCreateSettings{} + app.RegisterCommand( + "db-create", "Create new Stork database", databaseCreateSettings, + func() { + runDBCreate(databaseCreateSettings) + }, + ) - dbCreateFlags = append(dbCreateFlags, &cli.BoolFlag{ - Name: "force", - Usage: "Recreate the database and the user if they exist", - Aliases: []string{"f"}, - }) - - var dbVerFlags []cli.Flag - dbVerFlags = append(dbVerFlags, dbFlags...) - dbVerFlags = append(dbVerFlags, - &cli.StringFlag{ - Name: "version", - Usage: "Target database schema version (optional)", - Aliases: []string{"t"}, - EnvVars: []string{"STORK_TOOL_DB_VERSION"}, - }) + databasePasswordGenSettings := &cli.CommandSettings{} + app.RegisterCommand( + "db-password-gen", "Generate random Stork database password", + databasePasswordGenSettings, runDBPasswordGen, + ) - var certExportFlags []cli.Flag - certExportFlags = append(certExportFlags, dbFlags...) - certExportFlags = append(certExportFlags, - &cli.StringFlag{ - Name: "object", - Usage: "The object to dump; it can be one of 'cakey', 'cacert', 'srvkey', 'srvcert', 'srvtkn'", - Required: true, - Aliases: []string{"f"}, - EnvVars: []string{"STORK_TOOL_CERT_OBJECT"}, + databaseInitSettings := &databaseSettings{} + app.RegisterCommand( + "db-init", "Create schema versioning table in the database", + databaseInitSettings, func() { + runDBMigrate(databaseInitSettings.DatabaseSettings, "init", "") }, - &cli.StringFlag{ - Name: "file", - Usage: "The file location where the object should be saved; if not provided, then object is printed to stdout", - Aliases: []string{"o"}, - EnvVars: []string{"STORK_TOOL_CERT_FILE"}, - }) + ) - var certImportFlags []cli.Flag - certImportFlags = append(certImportFlags, dbFlags...) - certImportFlags = append(certImportFlags, - &cli.StringFlag{ - Name: "object", - Usage: "The object to dump; it can be one of 'cakey', 'cacert', 'srvkey', 'srvcert', 'srvtkn'", - Required: true, - Aliases: []string{"f"}, - EnvVars: []string{"STORK_TOOL_CERT_OBJECT"}, + databaseUpSettings := &databaseVersionSettings{} + app.RegisterCommand( + "db-up", "Run all available migrations or use -t to specify version", + databaseUpSettings, func() { + runDBMigrate( + databaseUpSettings.DatabaseSettings, + "up", + databaseUpSettings.Version, + ) }, - &cli.StringFlag{ - Name: "file", - Usage: "The file location from which the object will be read; if not provided, then the object is read from stdin", - Aliases: []string{"i"}, - EnvVars: []string{"STORK_TOOL_CERT_FILE"}, - }) + ) - hookInspectFlags := []cli.Flag{ - &cli.StringFlag{ - Name: "path", - Usage: "The hook file or directory path", - Required: true, - Aliases: []string{"p"}, - EnvVars: []string{"STORK_TOOL_HOOK_PATH"}, + databaseDownSettings := &databaseVersionSettings{} + app.RegisterCommand( + "db-down", "Revert last migration or use -t to specify version to downgrade to", + databaseDownSettings, func() { + runDBMigrate( + databaseDownSettings.DatabaseSettings, + "down", + databaseDownSettings.Version, + ) }, - } + ) - loginScreenWelcomeDeployFlags := []cli.Flag{ - &cli.StringFlag{ - Name: "file", - Usage: "HTML source file with a custom welcome message", - Required: true, - Aliases: []string{"i"}, - EnvVars: []string{"STORK_TOOL_LOGIN_SCREEN_WELCOME_FILE"}, - }, - &cli.StringFlag{ - Name: "rest-static-files-dir", - Usage: "The directory with static files for the UI; if not provided the tool will try to use default locations", - Aliases: []string{"d"}, - EnvVars: []string{"STORK_TOOL_REST_STATIC_FILES_DIR"}, + databaseResetSettings := &databaseSettings{} + app.RegisterCommand( + "db-reset", "Reset the database to the initial state", + databaseResetSettings, func() { + runDBMigrate(databaseResetSettings.DatabaseSettings, "reset", "") }, - } + ) - loginScreenWelcomeUndeployFlags := []cli.Flag{ - &cli.StringFlag{ - Name: "rest-static-files-dir", - Usage: "The directory with static files for the UI; if not provided the tool will try to use default locations", - Aliases: []string{"d"}, - EnvVars: []string{"STORK_TOOL_REST_STATIC_FILES_DIR"}, + dbVersionSettings := &databaseSettings{} + app.RegisterCommand( + "db-version", "Get the current database schema version", + dbVersionSettings, func() { + runDBMigrate(dbVersionSettings.DatabaseSettings, "version", "") }, - } - - cli.HelpFlag = &cli.BoolFlag{ - Name: "help", - Aliases: []string{"h"}, - Usage: "Show help", - } - - cli.VersionFlag = &cli.BoolFlag{ - Name: "version", - Aliases: []string{"v"}, - Usage: "Print the version", - } + ) - app := &cli.App{ - Name: "Stork Tool", - Usage: "A tool for managing Stork Server.", - Description: `The tool operates in four areas: + databaseSetVersionSettings := &databaseVersionSettings{} + app.RegisterCommand( + "db-set-version", "Set the database schema version", + databaseSetVersionSettings, func() { + runDBMigrate( + databaseSetVersionSettings.DatabaseSettings, + "set_version", + databaseSetVersionSettings.Version, + ) + }, + ) - - Certificate Management - it allows for exporting Stork Server keys, certificates, - and tokens that are used to secure communication between the Stork Server - and Stork Agents; + // Certificate management commands. + certificateExportSettings := &certificateExportSettings{} + app.RegisterCommand( + "cert-export", "Export Stork Server keys, certificates, and tokens", + certificateExportSettings, func() { + err := runCertExport(certificateExportSettings) + if err != nil { + log.WithError(err).Fatal("Failed to export the certificate") + } + }, + ) - - Database Creation - it facilitates creating a new database for the Stork Server, - and a user that can access this database with a generated password; + certificateImportSettings := &certificateImportSettings{} + app.RegisterCommand( + "cert-import", "Import Stork Server keys, certificates, and tokens", + certificateImportSettings, func() { + err := runCertImport(certificateImportSettings) + if err != nil { + log.WithError(err).Fatal("Failed to import the certificate") + } + }, + ) - - Database Migration - it allows for performing database schema migrations, - overwriting the db schema version and getting its current value; + // Hook inspection command. + hookInspectSettings := &hookInspectSettings{} + app.RegisterCommand( + "hook-inspect", "Inspect the hook file or directory", + hookInspectSettings, func() { + err := runHookInspect(hookInspectSettings) + if err != nil { + log.WithError(err).Fatal("Failed to inspect the hook") + } + }, + ) - - Static Views Deployment - it allows for setting custom content in selected - Stork views (e.g., custom welcome message on the login page).`, - Version: stork.Version, - HelpName: "stork-tool", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "", - Usage: "Logging level can be specified using env variable only. Allowed values: are DEBUG, INFO, WARN, ERROR", - Value: "INFO", - EnvVars: []string{"STORK_LOG_LEVEL"}, - }, + // Static views deployment commands. + loginScreenWelcomeDeploySettings := &loginScreenWelcomeDeploySettings{} + app.RegisterCommand( + "deploy-login-page-welcome", + "Deploy custom welcome message on the login screen", + loginScreenWelcomeDeploySettings, func() { + err := runStaticViewDeploy( + loginScreenWelcomeDeploySettings, "login-screen-welcome.html", + ) + if err != nil { + log.WithError(err). + Fatal("Failed to deploy the custom welcome message") + } }, - Commands: []*cli.Command{ - // DATABASE CREATION COMMANDS - { - Name: "db-create", - Usage: "Create new Stork database", - UsageText: "stork-tool db-create [options for db creation] -f", - Description: ``, - Flags: dbCreateFlags, - Category: "Database Creation", - Action: func(c *cli.Context) error { - runDBCreate(c) - return nil - }, - }, - { - Name: "db-password-gen", - Usage: "Generate random Stork database password", - UsageText: "stork-tool db-password-gen", - Description: ``, - Flags: []cli.Flag{}, - Category: "Database Creation", - Action: func(c *cli.Context) error { - runDBPasswordGen() - return nil - }, - }, - // DATABASE MIGRATION COMMANDS - { - Name: "db-init", - Usage: "Create schema versioning table in the database", - UsageText: "stork-tool db-init [options for db connection]", - Description: ``, - Flags: dbFlags, - Category: "Database Migration", - Action: func(c *cli.Context) error { - runDBMigrate(c, "init", "") - return nil - }, - }, - { - Name: "db-up", - Usage: "Run all available migrations or use -t to specify version", - UsageText: "stork-tool db-up [options for db connection] [-t version]", - Description: ``, - Flags: dbVerFlags, - Category: "Database Migration", - Action: func(c *cli.Context) error { - runDBMigrate(c, "up", c.String("version")) - return nil - }, - }, - { - Name: "db-down", - Usage: "Revert last migration or use -t to specify version to downgrade to", - UsageText: "stork-tool db-down [options for db connection] [-t version]", - Description: ``, - Flags: dbVerFlags, - Category: "Database Migration", - Action: func(c *cli.Context) error { - runDBMigrate(c, "down", c.String("version")) - return nil - }, - }, - { - Name: "db-reset", - Usage: "Revert all migrations", - UsageText: "stork-tool db-reset [options for db connection]", - Description: ``, - Flags: dbFlags, - Category: "Database Migration", - Action: func(c *cli.Context) error { - runDBMigrate(c, "reset", "") - return nil - }, - }, - { - Name: "db-version", - Usage: "Print current migration version", - UsageText: "stork-tool db-version [options for db connection]", - Description: ``, - Flags: dbFlags, - Category: "Database Migration", - Action: func(c *cli.Context) error { - runDBMigrate(c, "version", "") - return nil - }, - }, - { - Name: "db-set-version", - Usage: "Set database version without running migrations", - UsageText: "stork-tool db-set-version [options for db connection] [-t version]", - Description: ``, - Flags: dbVerFlags, - Category: "Database Migration", - Action: func(c *cli.Context) error { - runDBMigrate(c, "set_version", c.String("version")) - return nil - }, - }, - // CERTIFICATE MANAGEMENT - { - Name: "cert-export", - Usage: "Export certificate or other secret data", - UsageText: "stork-tool cert-export [options for db connection] [-f object] [-o filename]", - Description: ``, - Flags: certExportFlags, - Category: "Certificates Management", - Action: runCertExport, - }, - { - Name: "cert-import", - Usage: "Import certificate or other secret data", - UsageText: "stork-tool cert-import [options for db connection] [-f object] [-i filename]", - Description: ``, - Flags: certImportFlags, - Category: "Certificates Management", - Action: runCertImport, - }, - { - Name: "hook-inspect", - Usage: "Prints details about hooks", - UsageText: "stork-tool hook-inspect -p file-or-directory", - Description: "", - Flags: hookInspectFlags, - Action: runHookInspect, - }, - // STATIC VIEWS DEPLOYMENT - { - Name: "deploy-login-page-welcome", - Usage: "Deploy custom welcome message on the login page", - UsageText: "stork-tool deploy-login-page-welcome [-i filename] [-d directory]", - Description: ``, - Flags: loginScreenWelcomeDeployFlags, - Category: "Static Views Deployment", - Action: func(c *cli.Context) error { - return runStaticViewDeploy(c, "login-screen-welcome.html") - }, - }, - { - Name: "undeploy-login-page-welcome", - Usage: "Undeploy custom welcome message from the login page", - UsageText: "stork-tool undeploy-login-page-welcome [-d directory]", - Description: ``, - Flags: loginScreenWelcomeUndeployFlags, - Category: "Static Views Deployment", - Action: func(c *cli.Context) error { - return runStaticViewUndeploy(c, "login-screen-welcome.html") - }, - }, + ) + + loginScreenWelcomeUndeploySettings := &loginScreenWelcomeUndeploySettings{} + app.RegisterCommand( + "undeploy-login-page-welcome", + "Undeploy custom welcome message on the login screen", + loginScreenWelcomeUndeploySettings, func() { + err := runStaticViewUndeploy( + loginScreenWelcomeUndeploySettings, "login-screen-welcome.html", + ) + if err != nil { + log.WithError(err). + Fatal("Failed to undeploy the custom welcome message") + } }, - } + ) return app } +// The main function of the Stork tool. func main() { // Setup logging storkutil.SetupLogging() - app := setupApp() - err := app.Run(os.Args) + app := newApp() + err := app.Run("tool", os.Args[1:]) if err != nil { log.Fatal(err) } diff --git a/backend/cmd/stork-tool/main_test.go b/backend/cmd/stork-tool/main_test.go index 7870c5d21..6b256591c 100644 --- a/backend/cmd/stork-tool/main_test.go +++ b/backend/cmd/stork-tool/main_test.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path" + "regexp" "runtime" "strconv" "strings" @@ -23,6 +24,12 @@ import ( // Aux function checks if a list of expected strings is present in the string. func checkOutput(output string, exp []string, reason string) bool { + // The go-flags library wraps the output on certain width. We need to + // remove the dashes and newlines to make the search work because the + // os/exec Command function uses too narrow width. + pattern := regexp.MustCompile(`-\n\n\s+`) + output = pattern.ReplaceAllString(output, "") + for _, x := range exp { if !strings.Contains(output, x) { fmt.Printf("ERROR: Expected string \"%s\" not found in %s.\n", x, reason) @@ -107,7 +114,11 @@ func TestDbOptsHelp(t *testing.T) { require.NoError(t, err) // Now check that all expected command-line switches are really there. - require.True(t, checkOutput(string(output), dbOpts, "stork-tool * -h output")) + require.True(t, checkOutput( + string(output), + dbOpts, + fmt.Sprintf("stork-tool %s -h output", cmd), + )) } } @@ -128,6 +139,30 @@ func TestVersion(t *testing.T) { } } +// This test checks if stork-tool --version and -v report expected version. +// It doesn't call the binary. +func TestVersionStandalone(t *testing.T) { + // Arrange + app := newApp() + + for _, opt := range []string{"-v", "--version"} { + t.Run(opt, func(t *testing.T) { + args := []string{opt} + + // Act + var err error + stdout, _, captureErr := testutil.CaptureOutput(func() { + err = app.Run("tool", args) + }) + + // Assert + require.NoError(t, captureErr) + require.NoError(t, err) + require.Equal(t, stork.Version, strings.TrimSpace(string(stdout))) + }) + } +} + // Check if a db-* command can be invoked. func TestRunDBMigrate(t *testing.T) { _, settings, teardown := dbtest.SetupDatabaseTestCase(t) diff --git a/backend/go.mod b/backend/go.mod index 642d90803..92d76c608 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,42 +1,38 @@ module isc.org/stork -go 1.24.1 +go 1.23.1 require ( github.com/Showmax/go-fqdn v1.0.0 - github.com/alecthomas/participle/v2 v2.1.1 github.com/alexedwards/scs/v2 v2.8.0 github.com/apparentlymart/go-cidr v1.1.0 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 - github.com/go-openapi/errors v0.22.1 + github.com/brianvoe/gofakeit v3.18.0+incompatible + github.com/go-openapi/errors v0.22.0 github.com/go-openapi/loads v0.22.0 github.com/go-openapi/runtime v0.28.0 github.com/go-openapi/spec v0.21.0 github.com/go-openapi/strfmt v0.23.0 - github.com/go-openapi/swag v0.23.1 + github.com/go-openapi/swag v0.23.0 github.com/go-openapi/validate v0.24.0 github.com/go-pg/migrations/v8 v8.1.0 - github.com/go-pg/pg/v10 v10.14.0 - github.com/go-resty/resty/v2 v2.16.5 + github.com/go-pg/pg/v10 v10.13.0 github.com/jessevdk/go-flags v1.6.1 github.com/lib/pq v1.10.9 - github.com/miekg/dns v1.1.64 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.21.1 + github.com/prometheus/client_golang v1.20.4 github.com/prometheus/client_model v0.6.1 - github.com/prometheus/common v0.63.0 - github.com/shirou/gopsutil/v4 v4.25.2 + github.com/prometheus/common v0.59.1 + github.com/shirou/gopsutil/v4 v4.24.8 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.10.0 - github.com/urfave/cli/v2 v2.27.6 - go.uber.org/mock v0.5.0 - golang.org/x/net v0.37.0 - golang.org/x/sys v0.31.0 - golang.org/x/term v0.30.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 - google.golang.org/grpc v1.71.0 + github.com/stretchr/testify v1.9.0 + go.uber.org/mock v0.4.0 + golang.org/x/net v0.29.0 + golang.org/x/sys v0.25.0 + golang.org/x/term v0.24.0 + google.golang.org/grpc v1.67.0 google.golang.org/grpc/security/advancedtls v1.0.0 - google.golang.org/protobuf v1.36.6 + google.golang.org/protobuf v1.34.2 gopkg.in/h2non/gock.v1 v1.1.2 muzzammil.xyz/jsonc v1.0.0 ) @@ -44,45 +40,42 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/ebitengine/purego v0.8.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-pg/zerochecker v0.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/prometheus/procfs v0.16.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/vmihailenco/bufpool v0.1.11 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser v0.1.2 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.mongodb.org/mongo-driver v1.17.3 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect + go.mongodb.org/mongo-driver v1.17.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect mellium.im/sasl v0.3.2 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index ae2d92596..feb93fb4e 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -2,12 +2,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Showmax/go-fqdn v1.0.0 h1:0rG5IbmVliNT5O19Mfuvna9LL7zlHyRfsSvBPZmF9tM= github.com/Showmax/go-fqdn v1.0.0/go.mod h1:SfrFBzmDCtCGrnHhoDjuvFnKsWjEQX/Q9ARZvOrJAko= -github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= -github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= -github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= -github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= -github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= -github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= @@ -16,37 +10,31 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8= +github.com/brianvoe/gofakeit v3.18.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= -github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU= -github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= @@ -57,19 +45,17 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-pg/migrations/v8 v8.1.0 h1:bc1wQwFoWRKvLdluXCRFRkeaw9xDU4qJ63uCAagh66w= github.com/go-pg/migrations/v8 v8.1.0/go.mod h1:o+CN1u572XHphEHZyK6tqyg2GDkRvL2bIoLNyGIewus= github.com/go-pg/pg/v10 v10.4.0/go.mod h1:BfgPoQnD2wXNd986RYEHzikqv9iE875PrFaZ9vXvtNM= -github.com/go-pg/pg/v10 v10.14.0 h1:giXuPsJaWjzwzFJTxy39eBgGE44jpqH1jwv0uI3kBUU= -github.com/go-pg/pg/v10 v10.14.0/go.mod h1:6kizZh54FveJxw9XZdNg07x7DDBWNsQrSiJS04MLwO8= +github.com/go-pg/pg/v10 v10.13.0 h1:xMagDE57VP8Y2KvIf9PvrsOAIjX62XqaKmfEzB0c5eU= +github.com/go-pg/pg/v10 v10.13.0/go.mod h1:IXp9Ok9JNNW9yWedbQxxvKUv84XhoH5+tGd+68y+zDs= github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= -github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= -github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -84,22 +70,21 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= @@ -107,8 +92,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -120,12 +105,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= -github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ= -github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -149,39 +132,41 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= -github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= -github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= -github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= -github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= +github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= +github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk= -github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA= +github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI= +github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= -github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= -github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= @@ -193,40 +178,24 @@ github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vb github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= -go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.mongodb.org/mongo-driver v1.17.0 h1:Hp4q2MCjvY19ViwimTs00wHi7G4yzxh4/2+nTx8r40k= +go.mongodb.org/mongo-driver v1.17.0/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= go.opentelemetry.io/otel v0.13.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -239,14 +208,14 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -263,24 +232,20 @@ golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -290,13 +255,13 @@ google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc/examples v0.0.0-20201112215255-90f1b3ee835b h1:NuxyvVZoDfHZwYW9LD4GJiF5/nhiSyP4/InTrvw9Ibk= google.golang.org/grpc/examples v0.0.0-20201112215255-90f1b3ee835b/go.mod h1:IBqQ7wSUJ2Ep09a8rMWFsg4fmI2r38zwsq8a0GgxXpM= google.golang.org/grpc/security/advancedtls v1.0.0 h1:/KQ7VP/1bs53/aopk9QhuPyFAp9Dm9Ejix3lzYkCrDA= @@ -310,8 +275,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/server/cli.go b/backend/server/cli.go index 3416cf534..269db7a4e 100644 --- a/backend/server/cli.go +++ b/backend/server/cli.go @@ -1,14 +1,12 @@ package server import ( - "fmt" "os" - "strings" flags "github.com/jessevdk/go-flags" "github.com/pkg/errors" + "isc.org/stork/cli" "isc.org/stork/hooks" - "isc.org/stork/hooksutil" "isc.org/stork/server/agentcomm" dbops "isc.org/stork/server/database" "isc.org/stork/server/restservice" @@ -30,23 +28,8 @@ const ( VersionCommand Command = "version" ) -// Read environment file settings. It's parsed before the main settings. -type EnvironmentFileSettings struct { - EnvFile string `long:"env-file" description:"Environment file location; applicable only if the use-env-file is provided" default:"/etc/stork/server.env"` - UseEnvFile bool `long:"use-env-file" description:"Read the environment variables from the environment file"` -} - -// Read hook directory settings. They are parsed after environment file -// settings but before the main settings. -// It allows us to merge the hook flags with the core flags into a single output. -type HookDirectorySettings struct { - HookDirectory string `long:"hook-directory" description:"The path to the hook directory" env:"STORK_SERVER_HOOK_DIRECTORY" default:"/usr/lib/stork-server/hooks"` -} - // General server settings. type GeneralSettings struct { - EnvironmentFileSettings - HookDirectorySettings Version bool `short:"v" long:"version" description:"Show software version"` EnableMetricsEndpoint bool `short:"m" long:"metrics" description:"Enable Prometheus /metrics endpoint (no auth)" env:"STORK_SERVER_ENABLE_METRICS"` InitialPullerInterval int64 `long:"initial-puller-interval" description:"Initial interval used by pullers fetching data from Kea; if not provided the recommended values for each puller are used" env:"STORK_SERVER_INITIAL_PULLER_INTERVAL"` @@ -59,6 +42,7 @@ type Settings struct { AgentsSettings *agentcomm.AgentsSettings HooksSettings map[string]hooks.HookSettings DatabaseSettings *dbops.DatabaseSettings + HookDirectory string } // Constructs a new settings instance. @@ -74,282 +58,70 @@ func newSettings() *Settings { } } -// Stork server-specific CLI arguments/flags parser. -type CLIParser struct { - shortDescription string - longDescription string -} - -// Constructs CLI parser. -func NewCLIParser() *CLIParser { - return &CLIParser{ - shortDescription: "Stork Server", - longDescription: `Stork Server is a Kea and BIND 9 dashboard - -Stork logs on INFO level by default. Other levels can be configured using the -STORK_LOG_LEVEL variable. Allowed values are: DEBUG, INFO, WARN, ERROR.`, - } -} - // Parse the command line arguments into Stork-specific GO structures. // First, it parses the settings related to an environment file and if the file // is provided, the content is loaded. // Next, it parses the hooks location and extracts their CLI flags. // At the end, it composes the CLI parser from all the flags and runs it. -func (p *CLIParser) Parse() (command Command, settings *Settings, err error) { +func ParseCLIFlags() (command Command, settings *Settings, err error) { command = NoneCommand - - envFileSettings, err := p.parseEnvironmentFileSettings() - if err != nil { - return - } - - err = p.loadEnvironmentFile(envFileSettings) - if err != nil { - return - } - - hookDirectorySettings, err := p.parseHookDirectory() - if err != nil { - return - } - - allHookCLIFlags, err := p.collectHookCLIFlags(hookDirectorySettings) - if err != nil { - return - } - - settings, err = p.parseSettings(allHookCLIFlags) - if err != nil { - if isHelpRequest(err) { - return HelpCommand, nil, nil - } - return NoneCommand, nil, err - } - - if settings.GeneralSettings.Version { - // If user specified --version or -v, print the version and quit. - return VersionCommand, nil, nil - } - - return RunCommand, settings, nil -} - -// Check if a given error is a request to display the help. -func isHelpRequest(err error) bool { - var flagsError *flags.Error - if errors.As(err, &flagsError) { - if flagsError.Type == flags.ErrHelp { - return true - } - } - return false -} - -// Parses the CLI flags related to the environment file. -func (p *CLIParser) parseEnvironmentFileSettings() (*EnvironmentFileSettings, error) { - // Process command line flags. - // Process the environment file flag. - envFileSettings := &EnvironmentFileSettings{} - parser := flags.NewParser(envFileSettings, flags.IgnoreUnknown) - parser.ShortDescription = p.shortDescription - parser.LongDescription = p.longDescription - - if _, err := parser.Parse(); err != nil { - err = errors.Wrap(err, "invalid CLI argument") - return nil, err - } - return envFileSettings, nil -} - -// Loads the environment file content to the environment dictionary of the -// current process. -func (p *CLIParser) loadEnvironmentFile(envFileSettings *EnvironmentFileSettings) error { - if !envFileSettings.UseEnvFile { - // Nothing to do. - return nil - } - - err := storkutil.LoadEnvironmentFileToSetter( - envFileSettings.EnvFile, - storkutil.NewProcessEnvironmentVariableSetter(), - ) - if err != nil { - err = errors.WithMessagef(err, "invalid environment file: '%s'", envFileSettings.EnvFile) - return err - } - - // Reconfigures logging using new environment variables. - storkutil.SetupLogging() - - return nil -} - -// Parses the CLI flags related to the location of the hook directory. -func (p *CLIParser) parseHookDirectory() (*HookDirectorySettings, error) { - // Process the hook directory location. - hookDirectorySettings := &HookDirectorySettings{} - parser := flags.NewParser(hookDirectorySettings, flags.IgnoreUnknown) - parser.ShortDescription = p.shortDescription - parser.LongDescription = p.longDescription - - if _, err := parser.Parse(); err != nil { - err = errors.Wrap(err, "invalid CLI argument") - return nil, err - } - return hookDirectorySettings, nil -} - -// Extracts the CLI flags from the hooks. -func (p *CLIParser) collectHookCLIFlags(hookDirectorySettings *HookDirectorySettings) (map[string]hooks.HookSettings, error) { - allCLIFlags := map[string]hooks.HookSettings{} - stat, err := os.Stat(hookDirectorySettings.HookDirectory) - switch { - case err == nil && stat.IsDir(): - // Gather the hook flags. - hookWalker := hooksutil.NewHookWalker() - allCLIFlags, err = hookWalker.CollectCLIFlags( - hooks.HookProgramServer, - hookDirectorySettings.HookDirectory, - ) - if err != nil { - err = errors.WithMessage(err, "cannot collect the prototypes of the hook settings") - return nil, err - } - case err == nil && !stat.IsDir(): - // Hook directory is not a directory. - err = errors.Errorf( - "the provided hook directory path is not pointing to a directory: %s", - hookDirectorySettings.HookDirectory, - ) - return nil, err - case errors.Is(err, os.ErrNotExist): - // Hook directory doesn't exist. Skip and continue. - break - default: - // Unexpected problem. - err = errors.Wrapf(err, - "cannot stat the hook directory: %s", - hookDirectorySettings.HookDirectory, - ) - return nil, err - } - - return allCLIFlags, nil -} - -// Prepare conventional namespaces for the CLI flags and environment -// variables. -// CLI flags: -// - Have a component derived from the hook filename -// - Contains none upper cases, dots or spaces -// - Underscores are replaced with dashes -// -// Environment variables: -// - Starts with Stork-specific prefix -// - Have a component derived from the hook filename -// - Contains none lower cases, dots or spaces -// - Dashes are replaced with underscored -func getHookNamespaces(hookName string) (flagNamespace, envNamespace string) { - // Trim the app-specific prefix for simplicity. - hookName, _ = strings.CutPrefix(hookName, "stork-server-") - - // Replace all invalid characters with dashes. - hookName = strings.ReplaceAll(hookName, " ", "-") - hookName = strings.ReplaceAll(hookName, ".", "-") - - flagNamespace = strings.ReplaceAll(hookName, "_", "-") - flagNamespace = strings.ToLower(flagNamespace) - - // Prepend the common prefix for environment variables. - envNamespace = "STORK_SERVER_HOOK_" + strings.ReplaceAll(hookName, "-", "_") - envNamespace = strings.ToUpper(envNamespace) - return -} - -// Parses all CLI flags including the hooks-related ones. -func (p *CLIParser) parseSettings(allHooksCLIFlags map[string]hooks.HookSettings) (*Settings, error) { - settings := newSettings() + settings = newSettings() parser := flags.NewParser(settings.GeneralSettings, flags.Default) - parser.ShortDescription = p.shortDescription - parser.LongDescription = p.longDescription + parser.ShortDescription = "Stork Server" + parser.LongDescription = `Stork Server is a Kea and BIND 9 dashboard + +Stork logs on INFO level by default. Other levels can be configured using the +STORK_LOG_LEVEL variable. Allowed values are: DEBUG, INFO, WARN, ERROR.` databaseFlags := &dbops.DatabaseCLIFlags{} // Process Database specific args. - _, err := parser.AddGroup("Database ConnectionFlags", "", databaseFlags) + _, err = parser.AddGroup("Database ConnectionFlags", "", databaseFlags) if err != nil { err = errors.Wrap(err, "cannot add the database group") - return nil, err + return } // Process ReST API specific args. _, err = parser.AddGroup("HTTP ReST Server Flags", "", settings.RestAPISettings) if err != nil { err = errors.Wrap(err, "cannot add the ReST group") - return nil, err + return } // Process agent comm specific args. _, err = parser.AddGroup("Agents Communication Flags", "", settings.AgentsSettings) if err != nil { err = errors.Wrap(err, "cannot add the agents group") - return nil, err + return } - // Append hook flags. - for hookName, cliFlags := range allHooksCLIFlags { - if cliFlags == nil { - continue - } - group, err := parser.AddGroup(fmt.Sprintf("Hook '%s' Flags", hookName), "", cliFlags) - if err != nil { - err = errors.Wrapf(err, "invalid settings for the '%s' hook", hookName) - return nil, err - } + // Parse CLI flags. + appParser := cli.NewCLIParser(parser, "server", func() { + storkutil.SetupLogging() + }) - flagNamespace, envNamespace := getHookNamespaces(hookName) - group.EnvNamespace = envNamespace - group.Namespace = flagNamespace + hookDirSettings, hookFlags, isHelp, err := appParser.Parse(os.Args[1:]) + if err != nil { + return NoneCommand, nil, err } - // Check if there are no two groups with the same namespace. - // It may happen if the one of the hooks has the expected common prefix, - // but another one doesn't. For example, if we have two hooks named: - // - stork-server-ldap - // - ldap - // Both of them will have the same namespace: ldap. - // We suppose it will be a rare case, so we just return an error. - groupNamespaces := make(map[string]any) - for _, group := range parser.Groups() { - if group.Namespace == "" { - // Non-hook group. Skip. - continue - } - _, exist := groupNamespaces[group.Namespace] - if exist { - return nil, errors.Errorf( - "There are two hooks using the same configuration namespace "+ - "in the CLI flags: '%s'. The hook libraries for the "+ - "Stork server should use the following naming pattern, "+ - "e.g. 'stork-server-%s.so' instead of just '%s.so'", - group.Namespace, group.Namespace, group.Namespace, - ) - } - groupNamespaces[group.Namespace] = nil + if isHelp { + return HelpCommand, nil, nil } - // Do args parsing. - if _, err = parser.Parse(); err != nil { - err = errors.Wrap(err, "cannot parse the CLI flags") - return nil, err + if settings.GeneralSettings.Version { + // If user specified --version or -v, print the version and quit. + return VersionCommand, nil, nil } settings.DatabaseSettings, err = databaseFlags.ConvertToDatabaseSettings() if err != nil { - return nil, err + return NoneCommand, nil, err } - settings.HooksSettings = allHooksCLIFlags - return settings, nil + settings.HooksSettings = hookFlags + settings.HookDirectory = hookDirSettings.HookDirectory + + return RunCommand, settings, nil } diff --git a/backend/server/cli_test.go b/backend/server/cli_test.go index c0c442aa0..a91ab7b7c 100644 --- a/backend/server/cli_test.go +++ b/backend/server/cli_test.go @@ -2,28 +2,17 @@ package server import ( "os" - "path" "testing" "github.com/stretchr/testify/require" - "isc.org/stork/hooks" "isc.org/stork/testutil" ) -// Test that the CLI parser is constructed properly. -func TestNewCLIParser(t *testing.T) { - // Arrange & Act - parser := NewCLIParser() - - // Assert - require.NotNil(t, parser) -} - // Test that the environment variables from the environment file are loaded // and parsed by the CLI parser. func TestEnvironmentFileIsLoaded(t *testing.T) { // Arrange - restorePoint := testutil.CreateEnvironmentRestorePoint() + restorePoint := testutil.ClearEnvironmentVariables() defer restorePoint() sandbox := testutil.NewSandbox() defer sandbox.Close() @@ -41,10 +30,8 @@ func TestEnvironmentFileIsLoaded(t *testing.T) { "--env-file", envPath, } - parser := NewCLIParser() - // Act - command, settings, err := parser.Parse() + command, settings, err := ParseCLIFlags() // Assert require.NoError(t, err) @@ -52,14 +39,14 @@ func TestEnvironmentFileIsLoaded(t *testing.T) { require.Equal(t, RunCommand, command) require.Equal(t, "foo", settings.DatabaseSettings.Host) - require.Equal(t, "bar", settings.GeneralSettings.HookDirectory) + require.Equal(t, "bar", settings.HookDirectory) require.Equal(t, "baz", settings.RestAPISettings.Host) } // Test that the error is returned if the environment file is invalid. func TestEnvironmentFileIsInvalid(t *testing.T) { // Arrange - restorePoint := testutil.CreateEnvironmentRestorePoint() + restorePoint := testutil.ClearEnvironmentVariables() defer restorePoint() sandbox := testutil.NewSandbox() defer sandbox.Close() @@ -75,10 +62,8 @@ func TestEnvironmentFileIsInvalid(t *testing.T) { "--env-file", envPath, } - parser := NewCLIParser() - // Act - command, settings, err := parser.Parse() + command, settings, err := ParseCLIFlags() // Assert require.Error(t, err) @@ -91,7 +76,7 @@ func TestEnvironmentFileIsInvalid(t *testing.T) { func TestParseArgsFromMultipleSources(t *testing.T) { // Arrange // Environment variables - the lowest priority. - restore := testutil.CreateEnvironmentRestorePoint() + restore := testutil.ClearEnvironmentVariables() defer restore() os.Setenv("STORK_DATABASE_HOST", "database-host-envvar") @@ -115,9 +100,8 @@ func TestParseArgsFromMultipleSources(t *testing.T) { "--env-file", environmentFile.Name(), } - parser := NewCLIParser() // Act - command, settings, err := parser.Parse() + command, settings, err := ParseCLIFlags() // Assert require.NoError(t, err) @@ -132,10 +116,9 @@ func TestCLIParserRejectsWrongCLIArguments(t *testing.T) { // Arrange defer testutil.CreateOsArgsRestorePoint()() os.Args = []string{"stork-server", "--foo-bar-baz"} - parser := NewCLIParser() // Act - command, settings, err := parser.Parse() + command, settings, err := ParseCLIFlags() // Assert require.Error(t, err) @@ -143,61 +126,6 @@ func TestCLIParserRejectsWrongCLIArguments(t *testing.T) { require.EqualValues(t, NoneCommand, command) } -// Test that the namespaces are correct. -func TestHookNamespaces(t *testing.T) { - // Arrange - hookNames := []string{ - "foo", - "foo-bar", - "foo_bar", - "foo-42", - "foo-!@#", - "foo bar", - "foo.bar", - "FOO", - "fOo", - "FoO", - "stork-server-foo", - } - expectedFlagNamespaces := []string{ - "foo", - "foo-bar", - "foo-bar", - "foo-42", - "foo-!@#", - "foo-bar", - "foo-bar", - "foo", - "foo", - "foo", - "foo", - } - expectedEnvironmentNamespaces := []string{ - "STORK_SERVER_HOOK_FOO", - "STORK_SERVER_HOOK_FOO_BAR", - "STORK_SERVER_HOOK_FOO_BAR", - "STORK_SERVER_HOOK_FOO_42", - "STORK_SERVER_HOOK_FOO_!@#", - "STORK_SERVER_HOOK_FOO_BAR", - "STORK_SERVER_HOOK_FOO_BAR", - "STORK_SERVER_HOOK_FOO", - "STORK_SERVER_HOOK_FOO", - "STORK_SERVER_HOOK_FOO", - "STORK_SERVER_HOOK_FOO", - } - - for i := 0; i < len(hookNames); i++ { - hookName := hookNames[i] - t.Run(hookName, func(t *testing.T) { - // Act - flagNamespace, envNamespace := getHookNamespaces(hookName) - // Assert - require.Equal(t, expectedFlagNamespaces[i], flagNamespace) - require.Equal(t, expectedEnvironmentNamespaces[i], envNamespace) - }) - } -} - // Test that the error is returned if the hook directory path points to a file. func TestCollectHookCLIFlagsForNonDirectoryPath(t *testing.T) { // Arrange @@ -205,119 +133,13 @@ func TestCollectHookCLIFlagsForNonDirectoryPath(t *testing.T) { defer sandbox.Close() path, _ := sandbox.Join("file.ext") defer testutil.CreateOsArgsRestorePoint()() - parser := NewCLIParser() // Act os.Args = []string{"stork-server", "--hook-directory", path} - command, settings, err := parser.Parse() + command, settings, err := ParseCLIFlags() // Assert require.ErrorContains(t, err, "hook directory path is not pointing to a directory") require.Nil(t, settings) require.Equal(t, NoneCommand, command) } - -// Test that the no error is returned if the hook directory doesn't exist. -func TestCollectHookCLIFlagsForMissingDirectory(t *testing.T) { - // Arrange - sb := testutil.NewSandbox() - defer sb.Close() - parser := NewCLIParser() - hookSettings := &HookDirectorySettings{ - path.Join(sb.BasePath, "non-exists-directory"), - } - - // Act - flags, err := parser.collectHookCLIFlags(hookSettings) - - // Assert - require.NoError(t, err) - require.NotNil(t, flags) - require.Empty(t, flags) -} - -// Test that the hook settings are properly parsed from environment variables. -func TestParseHookSettingsFromEnvironmentVariables(t *testing.T) { - // Arrange - restore := testutil.CreateEnvironmentRestorePoint() - defer restore() - os.Setenv("STORK_SERVER_HOOK_BAZ_FOO_BAR", "fooBar") - - defer testutil.CreateOsArgsRestorePoint()() - os.Args = []string{"program-name"} - - type hookSettings struct { - FooBar string `long:"foo-bar" env:"FOO_BAR"` - } - - hookFlags := map[string]hooks.HookSettings{ - "baz": &hookSettings{}, - } - - parser := NewCLIParser() - - // Act - settings, err := parser.parseSettings(hookFlags) - - // Assert - require.NoError(t, err) - require.Contains(t, settings.HooksSettings, "baz") - require.Equal(t, "fooBar", settings.HooksSettings["baz"].(*hookSettings).FooBar) -} - -// Test that the hook settings are properly parsed from the CLI arguments. -func TestParseHookSettingsFromCLI(t *testing.T) { - // Arrange - defer testutil.CreateOsArgsRestorePoint()() - os.Args = []string{ - "program-name", - "--baz.foo-bar", "fooBar", - } - - type hookSettings struct { - FooBar string `long:"foo-bar" env:"FOO_BAR"` - } - - hookFlags := map[string]hooks.HookSettings{ - "baz": &hookSettings{}, - } - - parser := NewCLIParser() - - // Act - settings, err := parser.parseSettings(hookFlags) - - // Assert - require.NoError(t, err) - require.Contains(t, settings.HooksSettings, "baz") - require.Equal(t, "fooBar", settings.HooksSettings["baz"].(*hookSettings).FooBar) -} - -// Test that an error is returned if the two hooks are solved to the same -// namespace. -func TestPaseHookSettingsDuplicatedNamespace(t *testing.T) { - // Arrange - defer testutil.CreateOsArgsRestorePoint()() - os.Args = []string{ - "program-name", - "--baz.foo-bar", "fooBar", - } - - type hookSettings struct { - FooBar string `long:"foo-bar" env:"FOO_BAR"` - } - - hookFlags := map[string]hooks.HookSettings{ - "baz": &hookSettings{}, - "stork-server-baz": &hookSettings{}, - } - - parser := NewCLIParser() - - // Act - settings, err := parser.parseSettings(hookFlags) - - // Assert - require.ErrorContains(t, err, "two hooks using the same configuration namespace") - require.Nil(t, settings) -} diff --git a/backend/server/database/cli_test.go b/backend/server/database/cli_test.go index c0684462d..d5f4adf56 100644 --- a/backend/server/database/cli_test.go +++ b/backend/server/database/cli_test.go @@ -65,7 +65,7 @@ func TestSetFieldsBasedOnTags(t *testing.T) { // variables correctly. func TestReadFromEnvironment(t *testing.T) { // Arrange - restore := testutil.CreateEnvironmentRestorePoint() + restore := testutil.ClearEnvironmentVariables() defer restore() type mock struct { @@ -97,7 +97,7 @@ func TestReadFromEnvironment(t *testing.T) { // Test that the flags are read from the environment variables properly. func TestReadDatabaseCLIFlagsFromEnvironment(t *testing.T) { // Arrange - restore := testutil.CreateEnvironmentRestorePoint() + restore := testutil.ClearEnvironmentVariables() defer restore() os.Setenv("STORK_DATABASE_NAME", "dbname") @@ -130,7 +130,7 @@ func TestReadDatabaseCLIFlagsFromEnvironment(t *testing.T) { // Test that the maintenance flags are read from the environment variables properly. func TestReadMaintenanceDatabaseCLIFlagsFromEnvironment(t *testing.T) { // Arrange - restore := testutil.CreateEnvironmentRestorePoint() + restore := testutil.ClearEnvironmentVariables() defer restore() os.Setenv("STORK_DATABASE_MAINTENANCE_NAME", "maintenance-dbname") diff --git a/backend/server/server.go b/backend/server/server.go index 041556a71..3b5131952 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -57,6 +57,7 @@ type StorkServer struct { DaemonLocker config.DaemonLocker shutdownOnce sync.Once + HookDirectory string HookManager *hookmanager.HookManager hooksSettings map[string]hooks.HookSettings @@ -66,14 +67,14 @@ type StorkServer struct { // Parse the command line arguments into GO structures. // Returns the expected command to run and error. func (ss *StorkServer) ParseArgs() (command Command, err error) { - parser := NewCLIParser() - command, settings, err := parser.Parse() + command, settings, err := ParseCLIFlags() if command == RunCommand { ss.hooksSettings = settings.HooksSettings ss.AgentsSettings = *settings.AgentsSettings ss.DBSettings = *settings.DatabaseSettings ss.GeneralSettings = *settings.GeneralSettings ss.RestAPISettings = *settings.RestAPISettings + ss.HookDirectory = settings.HookDirectory } return command, err } @@ -98,19 +99,19 @@ func NewStorkServer() (ss *StorkServer, command Command, err error) { func (ss *StorkServer) Bootstrap(reload bool) (err error) { err = ss.HookManager.RegisterHooksFromDirectory( hooks.HookProgramServer, - ss.GeneralSettings.HookDirectory, + ss.HookDirectory, ss.hooksSettings, ) if err != nil { if errors.Is(err, os.ErrNotExist) { log. WithError(err). - Warnf("The hook directory: '%s' doesn't exist", ss.GeneralSettings.HookDirectory) + Warnf("The hook directory: '%s' doesn't exist", ss.HookDirectory) } else { return errors.WithMessagef( err, "failed to load hooks from directory: '%s'", - ss.GeneralSettings.HookDirectory, + ss.HookDirectory, ) } } diff --git a/backend/server/server_test.go b/backend/server/server_test.go index 6786d3bed..57f236617 100644 --- a/backend/server/server_test.go +++ b/backend/server/server_test.go @@ -160,7 +160,7 @@ func TestNewStorkServer(t *testing.T) { require.EqualValues(t, "tlsca", ss.RestAPISettings.TLSCACertificate) require.EqualValues(t, "staticdir", ss.RestAPISettings.StaticFilesDir) require.EqualValues(t, 54, ss.GeneralSettings.InitialPullerInterval) - require.EqualValues(t, "hookdir", ss.GeneralSettings.HookDirectory) + require.EqualValues(t, "hookdir", ss.HookDirectory) } // Test that the Stork Server is constructed if no arguments are provided. diff --git a/backend/testutil/utils.go b/backend/testutil/utils.go index 18e9c6c21..e97d35c4d 100644 --- a/backend/testutil/utils.go +++ b/backend/testutil/utils.go @@ -80,40 +80,20 @@ func ParseTimestampFilename(filename string) (prefix string, timestamp time.Time return } -// Allows reverting the changes in the environment variables to a previous -// state. It remembers the current environment variables and returns a function +// Clears the environment variables and allows reverting the original state. +// It remembers the current environment variables and returns a function // that must be called to restore these values. -func CreateEnvironmentRestorePoint() func() { +func ClearEnvironmentVariables() func() { originalEnv := os.Environ() + os.Clearenv() + return func() { - originalEnvDict := make(map[string]string, len(originalEnv)) + os.Clearenv() + for _, pair := range originalEnv { key, value, _ := strings.Cut(pair, "=") - originalEnvDict[key] = value - } - - actualEnv := os.Environ() - actualKeys := make(map[string]bool, len(actualEnv)) - for _, actualPair := range actualEnv { - actualKey, actualValue, _ := strings.Cut(actualPair, "=") - actualKeys[actualKey] = true - originalValue, exist := originalEnvDict[actualKey] - - if !exist { - // Environment variable was added. - os.Unsetenv(actualKey) - } else if actualValue != originalValue { - // Environment variable was changed. - os.Setenv(actualKey, originalValue) - } - } - - for originalKey, originalValue := range originalEnvDict { - if _, exist := actualKeys[originalKey]; !exist { - // Environment variable was removed. - os.Setenv(originalKey, originalValue) - } + os.Setenv(key, value) } } } diff --git a/backend/testutil/utils_test.go b/backend/testutil/utils_test.go index ce230206e..4f5e2c689 100644 --- a/backend/testutil/utils_test.go +++ b/backend/testutil/utils_test.go @@ -186,15 +186,15 @@ func TestParseTimestampFilenameDoubleExtension(t *testing.T) { require.EqualValues(t, ".bar.baz", extension) } -// Test that the restore point clears the environment variables. -func TestCreateEnvironmentRestorePoint(t *testing.T) { +// Test that the restore point revers the environment variables. +func TestClearEnvironmentVariables(t *testing.T) { // Arrange os.Unsetenv("STORK_TEST_KEY1") os.Setenv("STORK_TEST_KEY2", "foo") os.Setenv("STORK_TEST_KEY3", "bar") // Act - restore := CreateEnvironmentRestorePoint() + restore := ClearEnvironmentVariables() os.Setenv("STORK_TEST_KEY1", "baz") os.Unsetenv("STORK_TEST_KEY2") os.Setenv("STORK_TEST_KEY3", "boz") diff --git a/backend/util/envfile.go b/backend/util/envfile.go index 0fa0fe9d8..91613b900 100644 --- a/backend/util/envfile.go +++ b/backend/util/envfile.go @@ -9,12 +9,6 @@ import ( "github.com/pkg/errors" ) -// Stores key-value of the environment variable. -type keyValuePair struct { - key string - value string -} - // Defines an interface that accepts the environment variables. type EnvironmentVariableSetter interface { Set(key, value string) error @@ -37,20 +31,17 @@ func (s *processEnvironmentVariableSetter) Set(key, value string) error { // Loads all entries from the environment file into one or multiple setters. func LoadEnvironmentFileToSetter(path string, setters ...EnvironmentVariableSetter) error { - data, err := loadEnvironmentFile(path) + data, err := LoadEnvironmentFile(path) if err != nil { return err } - for _, pair := range data { + for key, value := range data { for _, setter := range setters { - err = setter.Set(pair.key, pair.value) + err = setter.Set(key, value) if err != nil { err = errors.WithMessagef( - err, - "cannot set '%s=%s' environment variable", - pair.key, - pair.value, + err, "cannot set '%s=%s' environment variable", key, value, ) return err } @@ -61,7 +52,7 @@ func LoadEnvironmentFileToSetter(path string, setters ...EnvironmentVariableSett } // Loads all entries from the environment file. -func loadEnvironmentFile(path string) ([]*keyValuePair, error) { +func LoadEnvironmentFile(path string) (map[string]string, error) { file, err := os.Open(path) if err != nil { return nil, errors.Wrapf(err, "cannot open the '%s' environment file", path) @@ -71,9 +62,8 @@ func loadEnvironmentFile(path string) ([]*keyValuePair, error) { } // Loads all entries from a given reader. -func loadEnvironmentEntries(reader io.Reader) ([]*keyValuePair, error) { - data := []*keyValuePair{} - dataIndex := map[string]*keyValuePair{} +func loadEnvironmentEntries(reader io.Reader) (map[string]string, error) { + dataIndex := map[string]string{} scanner := bufio.NewScanner(reader) lineIdx := 0 @@ -88,16 +78,10 @@ func loadEnvironmentEntries(reader io.Reader) ([]*keyValuePair, error) { continue } - if pair, ok := dataIndex[key]; ok { - pair.value = value - } else { - pair := &keyValuePair{key, value} - data = append(data, pair) - dataIndex[key] = pair - } + dataIndex[key] = value } - return data, nil + return dataIndex, nil } // Parses a line of the environment file. diff --git a/backend/util/envfile_test.go b/backend/util/envfile_test.go index 22caafa57..7b75b51cf 100644 --- a/backend/util/envfile_test.go +++ b/backend/util/envfile_test.go @@ -16,7 +16,7 @@ func TestLoadMissingEnvironmentFile(t *testing.T) { // Arrange & Act sb := testutil.NewSandbox() defer sb.Close() - data, err := loadEnvironmentFile(path.Join(sb.BasePath, "not-exists.env")) + data, err := LoadEnvironmentFile(path.Join(sb.BasePath, "not-exists.env")) // Assert require.Error(t, err) @@ -34,8 +34,8 @@ func TestLoadSingleLineEnvironmentContent(t *testing.T) { // Assert require.NoError(t, err) require.Len(t, data, 1) - require.EqualValues(t, "TEST_STORK_KEY", data[0].key) - require.EqualValues(t, "VALUE", data[0].value) + require.Contains(t, data, "TEST_STORK_KEY") + require.EqualValues(t, "VALUE", data["TEST_STORK_KEY"]) } // Test that the multi-line environment file content is loaded properly. @@ -51,12 +51,12 @@ func TestLoadMultiLineEnvironmentContent(t *testing.T) { // Assert require.NoError(t, err) require.Len(t, data, 3) - require.EqualValues(t, "TEST_STORK_KEY1", data[0].key) - require.EqualValues(t, "VALUE1", data[0].value) - require.EqualValues(t, "TEST_STORK_KEY2", data[1].key) - require.EqualValues(t, "VALUE2", data[1].value) - require.EqualValues(t, "TEST_STORK_KEY3", data[2].key) - require.EqualValues(t, "VALUE3", data[2].value) + require.Contains(t, data, "TEST_STORK_KEY1") + require.EqualValues(t, "VALUE1", data["TEST_STORK_KEY1"]) + require.Contains(t, data, "TEST_STORK_KEY2") + require.EqualValues(t, "VALUE2", data["TEST_STORK_KEY2"]) + require.Contains(t, data, "TEST_STORK_KEY3") + require.EqualValues(t, "VALUE3", data["TEST_STORK_KEY3"]) } // Test that the duplicates in the content are overwritten properly. @@ -72,8 +72,8 @@ func TestLoadEnvironmentContentWithDuplicates(t *testing.T) { // Assert require.NoError(t, err) require.Len(t, data, 1) - require.EqualValues(t, "TEST_STORK_KEY1", data[0].key) - require.EqualValues(t, "VALUE3", data[0].value) + require.Contains(t, data, "TEST_STORK_KEY1") + require.EqualValues(t, "VALUE3", data["TEST_STORK_KEY1"]) } // Test that the empty value in the environment file content is loaded properly. @@ -87,8 +87,8 @@ func TestLoadEnvironmentContentWithEmptyValue(t *testing.T) { // Assert require.Len(t, data, 1) require.NoError(t, err) - require.EqualValues(t, "TEST_STORK_KEY", data[0].key) - require.EqualValues(t, "", data[0].value) + require.Contains(t, data, "TEST_STORK_KEY") + require.EqualValues(t, "", data["TEST_STORK_KEY"]) } // Test that the missing value separator in the environment file content @@ -146,8 +146,8 @@ func TestLoadEnvironmentContentWithComments(t *testing.T) { // Assert require.NoError(t, err) require.Len(t, data, 1) - require.EqualValues(t, "TEST_STORK_KEY2", data[0].key) - require.EqualValues(t, "VALUE2", data[0].value) + require.Contains(t, data, "TEST_STORK_KEY2") + require.EqualValues(t, "VALUE2", data["TEST_STORK_KEY2"]) } // Test that the empty lines are skipped. @@ -162,10 +162,10 @@ func TestLoadEnvironmentContentWithEmptyLine(t *testing.T) { // Assert require.NoError(t, err) - require.EqualValues(t, "TEST_STORK_KEY1", data[0].key) - require.EqualValues(t, "VALUE1", data[0].value) - require.EqualValues(t, "TEST_STORK_KEY2", data[1].key) - require.EqualValues(t, "VALUE2", data[1].value) + require.Contains(t, data, "TEST_STORK_KEY1") + require.EqualValues(t, "VALUE1", data["TEST_STORK_KEY1"]) + require.Contains(t, data, "TEST_STORK_KEY2") + require.EqualValues(t, "VALUE2", data["TEST_STORK_KEY2"]) require.Len(t, data, 2) } @@ -182,8 +182,8 @@ func TestLoadEnvironmentContentWithTrailingCharacters(t *testing.T) { // Assert require.NoError(t, err) require.Len(t, data, 1) - require.EqualValues(t, "TEST_STORK_KEY2", data[0].key) - require.EqualValues(t, "VALUE2", data[0].value) + require.Contains(t, data, "TEST_STORK_KEY2") + require.EqualValues(t, "VALUE2", data["TEST_STORK_KEY2"]) } type setterMock struct { @@ -242,7 +242,7 @@ func TestLoadEnvironmentVariablesToSetterWithError(t *testing.T) { // Assert require.ErrorContains(t, err, "foo") - require.NotContains(t, mock.data, "TEST_STORK_KEY2") + require.Len(t, mock.data, 1) } // Test that the process setter is constructed properly. @@ -257,7 +257,7 @@ func TestNewProcessEnvironmentVariableSetter(t *testing.T) { // Test that the process setter sets the key-value pair properly. func TestProcessEnvironmentVariableSetterAcceptsValidPair(t *testing.T) { // Arrange - t.Cleanup(testutil.CreateEnvironmentRestorePoint()) + t.Cleanup(testutil.ClearEnvironmentVariables()) setter := NewProcessEnvironmentVariableSetter() // Act @@ -272,7 +272,7 @@ func TestProcessEnvironmentVariableSetterAcceptsValidPair(t *testing.T) { // Test that process setter returns an error on invalid key-value pair. func TestProcessEnvironmentVariableSetterRejectInvalidPair(t *testing.T) { // Arrange - t.Cleanup(testutil.CreateEnvironmentRestorePoint()) + t.Cleanup(testutil.ClearEnvironmentVariables()) setter := NewProcessEnvironmentVariableSetter() // Act diff --git a/backend/util/util_test.go b/backend/util/util_test.go index 25082c76a..99bc5b7f1 100644 --- a/backend/util/util_test.go +++ b/backend/util/util_test.go @@ -454,8 +454,8 @@ func TestLoggingLevel(t *testing.T) { {env: "-", lv: log.InfoLevel}, } - // Let's remember state of the environment and revert to it after test. - restore := testutil.CreateEnvironmentRestorePoint() + // Let's clear the environment and revert to it after test. + restore := testutil.ClearEnvironmentVariables() defer restore() for _, test := range testCases { diff --git a/changelog_unreleased/1587-stork-agent-fails-without-descriptive-result.md b/changelog_unreleased/1587-stork-agent-fails-without-descriptive-result.md new file mode 100644 index 000000000..599df841c --- /dev/null +++ b/changelog_unreleased/1587-stork-agent-fails-without-descriptive-result.md @@ -0,0 +1,6 @@ +[func] slawek + + Added verification of the environment variables provided by a shell + and in the environment file to print a descriptive feedback in case + of a typo or using a deprecated variable. + (Gitlab #1587)