Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a7e2329
[#1587] Exclude a common part of CLI parser
fivitti Jan 2, 2025
ac04f65
[#1587] Working general CLI parser
fivitti Jan 3, 2025
ffd2cf4
[#1587] Fix the wrong argument
fivitti Jan 7, 2025
8808118
[#1587] Unify agent and server parsers
fivitti Jan 7, 2025
e301a43
[#1587] Verify the envvars
fivitti Jan 7, 2025
30c4d21
[#1587] Fix linter issues
fivitti Jan 7, 2025
c8ca1c5
[#1587] Verify system environment variables
fivitti Jan 7, 2025
81b857a
[#1587] Add unit tests
fivitti Jan 7, 2025
48add73
[#1587] Extend unit test
fivitti Jan 7, 2025
06ba0b6
[#1587] Fix unit tests
fivitti Jan 7, 2025
cfb6ec4
[#1587] Simplify utility
fivitti Jan 7, 2025
7a29eb8
[#1587] Fix linter issue
fivitti Jan 8, 2025
522be1b
[#1587] Remove redundant flags
fivitti Jan 8, 2025
c6657d4
[#1587] Add unit tests
fivitti Jan 8, 2025
bf98f00
[#1587] Add a Changelog entry
fivitti Jan 8, 2025
19b7c94
[#1587] Unify the CLI handling in the Stork tool
fivitti Jan 8, 2025
66db02f
[#1587] Move package
fivitti Jan 8, 2025
425910d
[#1587] Exclude app to a separate file
fivitti Jan 8, 2025
7b201bd
[#1587] Unexport structs
fivitti Jan 8, 2025
6569020
[#1587] Unify code-gen CLI
fivitti Jan 8, 2025
27c6b7c
[#1587] Remove unnecessary dependencies
fivitti Jan 8, 2025
185123d
[#1587] Rename structs
fivitti Jan 9, 2025
f1d16f2
[#1587] Add unit tests
fivitti Jan 9, 2025
60f2fe4
[#1587] Rephrase a sentence
fivitti Jan 9, 2025
1acfd8d
[#1587] Support hooks only for agent and server
fivitti Jan 9, 2025
9285a3c
[#1587] Add unit test
fivitti Jan 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions backend/cli/app.go
Original file line number Diff line number Diff line change
@@ -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/[email protected]/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)
}
140 changes: 140 additions & 0 deletions backend/cli/app_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading