From fc16493430c73cc7c678e5d971f62183e14b5616 Mon Sep 17 00:00:00 2001 From: Tullio Sebastiani Date: Tue, 15 Oct 2024 18:05:27 +0200 Subject: [PATCH] input validation wip --- cmd/describe.go | 47 +++++++-- cmd/list.go | 16 +-- cmd/root.go | 26 +---- cmd/run.go | 99 +++++++++++++++++++ cmd/tables.go | 20 +++- cmd/utils.go | 48 +++++++++ pkg/provider/models/models.go | 9 ++ .../quay/quay_scenario_provider_test.go | 4 + pkg/text/text_justify.go | 52 ++++++++++ 9 files changed, 277 insertions(+), 44 deletions(-) create mode 100644 cmd/run.go create mode 100644 cmd/utils.go create mode 100644 pkg/text/text_justify.go diff --git a/cmd/describe.go b/cmd/describe.go index 63a0390..7d4ab6a 100644 --- a/cmd/describe.go +++ b/cmd/describe.go @@ -1,8 +1,13 @@ package cmd import ( + "fmt" + "github.com/fatih/color" "github.com/krkn-chaos/krknctl/pkg/provider/factory" + "github.com/krkn-chaos/krknctl/pkg/provider/models" + "github.com/krkn-chaos/krknctl/pkg/text" "github.com/spf13/cobra" + "log" ) func NewDescribeCommand(factory *factory.ProviderFactory) *cobra.Command { @@ -18,21 +23,49 @@ func NewDescribeCommand(factory *factory.ProviderFactory) *cobra.Command { return []string{}, cobra.ShellCompDirectiveError } provider := GetProvider(offline, factory) - scenarios, err := provider.GetScenarios() + + scenarios, err := FetchScenarios(provider) if err != nil { + log.Fatalf("Error fetching scenarios: %v", err) return []string{}, cobra.ShellCompDirectiveError } - var foundScenarios []string - for _, scenario := range *scenarios { - foundScenarios = append(foundScenarios, scenario.Name) - } - return foundScenarios, cobra.ShellCompDirectiveNoFileComp + + return *scenarios, cobra.ShellCompDirectiveNoFileComp }, RunE: func(cmd *cobra.Command, args []string) error { - + offline, err := cmd.Flags().GetBool("offline") + if err != nil { + return err + } + spinner := NewSpinnerWithSuffix("fetching scenario details...") + spinner.Start() + provider := GetProvider(offline, factory) + scenarioDetail, err := provider.GetScenarioDetail(args[0]) + if err != nil { + return err + } + spinner.Stop() + if scenarioDetail == nil { + return fmt.Errorf("could not find %s scenario", args[0]) + } + PrintScenarioDetail(scenarioDetail) return nil }, } return describeCmd } + +func PrintScenarioDetail(scenarioDetail *models.ScenarioDetail) { + fmt.Print("\n") + _, _ = color.New(color.FgGreen, color.Underline).Println(scenarioDetail.Name) + justifiedText := text.Justify(scenarioDetail.Description, 50) + for _, line := range justifiedText { + fmt.Println(line) + } + fmt.Print("\n") + argumentTable := NewArgumentTable(scenarioDetail.Fields) + argumentTable.Print() + fmt.Print("\n") + +} diff --git a/cmd/list.go b/cmd/list.go index 55515fa..6255d10 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" provider_factory "github.com/krkn-chaos/krknctl/pkg/provider/factory" "github.com/spf13/cobra" "log" @@ -27,20 +28,11 @@ func NewListCommand(factory *provider_factory.ProviderFactory) *cobra.Command { log.Fatalf("failed to fetch scenarios: %v", err) } s.Stop() - test := NewScenarioTable(scenarios) - (*test).Print() + scenarioTable := NewScenarioTable(scenarios) + scenarioTable.Print() + fmt.Print("\n") return nil }, } return listCmd } - -func GetProvider(offline bool, factory *provider_factory.ProviderFactory) provider_factory.ScenarioDataProvider { - var provider provider_factory.ScenarioDataProvider - if offline { - provider = factory.NewInstance(provider_factory.Offline) - } else { - provider = factory.NewInstance(provider_factory.Online) - } - return provider -} diff --git a/cmd/root.go b/cmd/root.go index d34a2fa..eae693e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,31 +2,10 @@ package cmd import ( "fmt" - "github.com/briandowns/spinner" "github.com/krkn-chaos/krknctl/pkg/provider/factory" - "github.com/spf13/cobra" "os" - "time" ) -func NewSpinnerWithSuffix(suffix string) *spinner.Spinner { - s := spinner.New(spinner.CharSets[39], 100*time.Millisecond) - s.Suffix = suffix - return s -} - -func NewRootCommand(factory *factory.ProviderFactory) *cobra.Command { - var rootCmd = &cobra.Command{ - Use: "krknctl", - Short: "krkn CLI", - Long: `krkn Command Line Interface`, - RunE: func(cmd *cobra.Command, args []string) error { - return cmd.Help() - }, - } - return rootCmd -} - func Execute(factory *factory.ProviderFactory) { var jsonFlag bool var offlineFlag bool @@ -42,6 +21,11 @@ func Execute(factory *factory.ProviderFactory) { describeCmd := NewDescribeCommand(factory) rootCmd.AddCommand(describeCmd) + + runCmd := NewRunCommand(factory) + runCmd.DisableFlagParsing = true + rootCmd.AddCommand(runCmd) + if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..f865b0d --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,99 @@ +package cmd + +import ( + "fmt" + "github.com/krkn-chaos/krknctl/pkg/provider/factory" + "github.com/spf13/cobra" + "log" + "strings" +) + +func NewRunCommand(factory *factory.ProviderFactory) *cobra.Command { + collectedFlags := make(map[string]*string) + var runCmd = &cobra.Command{ + Use: "run", + Short: "runs a scenario", + Long: `runs a scenario`, + DisableFlagParsing: false, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + offline, err := cmd.Flags().GetBool("offline") + //offlineRepo, err := cmd.Flags().GetString("offline-repo-config") + if err != nil { + return []string{}, cobra.ShellCompDirectiveError + } + provider := GetProvider(offline, factory) + scenarios, err := FetchScenarios(provider) + if err != nil { + log.Fatalf("Error fetching scenarios: %v", err) + return []string{}, cobra.ShellCompDirectiveError + } + return *scenarios, cobra.ShellCompDirectiveNoFileComp + }, + + PreRunE: func(cmd *cobra.Command, args []string) error { + offline, err := cmd.Flags().GetBool("offline") + if err != nil { + return err + } + provider := GetProvider(offline, factory) + scenarioDetail, err := provider.GetScenarioDetail(args[0]) + if err != nil { + return err + } + if scenarioDetail == nil { + return fmt.Errorf("%s scenario not found", args[0]) + } + + for _, field := range scenarioDetail.Fields { + var defaultValue string = "" + if field.Default != nil { + defaultValue = *field.Default + } + collectedFlags[*field.Name] = cmd.LocalFlags().String(*field.Name, defaultValue, *field.Description) + if err != nil { + return err + } + + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + spinner := NewSpinnerWithSuffix("validating input...") + offline, err := cmd.Flags().GetBool("offline") + if err != nil { + return err + } + spinner.Start() + + provider := GetProvider(offline, factory) + scenarioDetail, err := provider.GetScenarioDetail(args[0]) + if err != nil { + return err + } + spinner.Stop() + // default + for k, _ := range collectedFlags { + field := scenarioDetail.GetFieldByName(k) + var foundArg *string = nil + for i, a := range args { + if a == fmt.Sprintf("--%s", k) { + if len(args) < i+2 || strings.HasPrefix(args[i+1], "--") { + return fmt.Errorf("%s has no value", args[i]) + } + foundArg = &args[i+1] + } + } + if field != nil { + value, err := field.Validate(foundArg) + if err != nil { + return err + } + fmt.Println(fmt.Sprintf("%s: valid", *value)) + } + + } + return nil + }, + } + return runCmd +} diff --git a/cmd/tables.go b/cmd/tables.go index 2a84401..a570302 100644 --- a/cmd/tables.go +++ b/cmd/tables.go @@ -1,18 +1,30 @@ package cmd import ( + "fmt" "github.com/fatih/color" "github.com/krkn-chaos/krknctl/pkg/provider/models" + "github.com/krkn-chaos/krknctl/pkg/typing" ) import "github.com/rodaine/table" -func NewScenarioTable(scenarios *[]models.ScenarioTag) *table.Table { - headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc() - columnFmt := color.New(color.FgYellow).SprintfFunc() +var headerFmt = color.New(color.FgGreen, color.Underline).SprintfFunc() +var columnFmt = color.New(color.FgYellow).SprintfFunc() + +func NewScenarioTable(scenarios *[]models.ScenarioTag) table.Table { tbl := table.New("Name", "Size", "Digest", "Last Modified") tbl.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt) for _, scenario := range *scenarios { tbl.AddRow(scenario.Name, scenario.Size, scenario.Digest, scenario.LastModified) } - return &tbl + return tbl +} + +func NewArgumentTable(inputFields []typing.InputField) table.Table { + tbl := table.New("Name", "Type", "Description", "Required") + tbl.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt) + for _, inputField := range inputFields { + tbl.AddRow(fmt.Sprintf("--%s", *inputField.Name), inputField.Type.String(), *inputField.ShortDescription, inputField.Default == nil) + } + return tbl } diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 0000000..fcaca20 --- /dev/null +++ b/cmd/utils.go @@ -0,0 +1,48 @@ +package cmd + +import ( + "github.com/briandowns/spinner" + "github.com/krkn-chaos/krknctl/pkg/provider/factory" + "github.com/spf13/cobra" + "time" +) + +func NewSpinnerWithSuffix(suffix string) *spinner.Spinner { + s := spinner.New(spinner.CharSets[39], 100*time.Millisecond) + s.Suffix = suffix + return s +} + +func NewRootCommand(factory *factory.ProviderFactory) *cobra.Command { + var rootCmd = &cobra.Command{ + Use: "krknctl", + Short: "krkn CLI", + Long: `krkn Command Line Interface`, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } + return rootCmd +} + +func GetProvider(offline bool, providerFactory *factory.ProviderFactory) factory.ScenarioDataProvider { + var provider factory.ScenarioDataProvider + if offline { + provider = providerFactory.NewInstance(factory.Offline) + } else { + provider = providerFactory.NewInstance(factory.Online) + } + return provider +} + +func FetchScenarios(provider factory.ScenarioDataProvider) (*[]string, error) { + scenarios, err := provider.GetScenarios() + if err != nil { + return nil, err + } + var foundScenarios []string + for _, scenario := range *scenarios { + foundScenarios = append(foundScenarios, scenario.Name) + } + return &foundScenarios, nil +} diff --git a/pkg/provider/models/models.go b/pkg/provider/models/models.go index 39b9eb7..0167e08 100644 --- a/pkg/provider/models/models.go +++ b/pkg/provider/models/models.go @@ -18,3 +18,12 @@ type ScenarioDetail struct { Description string `json:"description"` Fields []typing.InputField } + +func (s *ScenarioDetail) GetFieldByName(name string) *typing.InputField { + for _, v := range s.Fields { + if *v.Name == name { + return &v + } + } + return nil +} diff --git a/pkg/provider/quay/quay_scenario_provider_test.go b/pkg/provider/quay/quay_scenario_provider_test.go index 9a2f5b6..c5fcc70 100644 --- a/pkg/provider/quay/quay_scenario_provider_test.go +++ b/pkg/provider/quay/quay_scenario_provider_test.go @@ -75,4 +75,8 @@ func TestQuayScenarioProvider_GetScenarioDetail(t *testing.T) { assert.True(t, strings.Contains(err.Error(), "krkn.inputfields LABEL not found in tag: cpu-memory-noinput")) assert.Nil(t, scenario) + scenario, err = provider.GetScenarioDetail("not-found") + assert.Nil(t, err) + assert.Nil(t, scenario) + } diff --git a/pkg/text/text_justify.go b/pkg/text/text_justify.go new file mode 100644 index 0000000..d57c0ba --- /dev/null +++ b/pkg/text/text_justify.go @@ -0,0 +1,52 @@ +package text + +import "strings" + +func justifyLine(words []string, width int) string { + if len(words) == 1 { + return words[0] + strings.Repeat(" ", width-len(words[0])) + } + totalSpaces := width + for _, word := range words { + totalSpaces -= len(word) + } + spacesBetweenWords := totalSpaces / (len(words) - 1) + extraSpaces := totalSpaces % (len(words) - 1) + + var justifiedLine string + for i, word := range words { + justifiedLine += word + if i < len(words)-1 { + spaceCount := spacesBetweenWords + if i < extraSpaces { + spaceCount++ + } + justifiedLine += strings.Repeat(" ", spaceCount) + } + } + return justifiedLine +} + +func Justify(text string, width int) []string { + words := strings.Fields(text) + var result []string + var line []string + lineLength := 0 + + for _, word := range words { + if lineLength+len(word)+len(line) > width { + result = append(result, justifyLine(line, width)) + line = []string{} + lineLength = 0 + } + line = append(line, word) + lineLength += len(word) + } + + if len(line) > 0 { + lastLine := strings.Join(line, " ") + result = append(result, lastLine+strings.Repeat(" ", width-len(lastLine))) + } + + return result +}