From 4a982a80f0da0c065ffa92bc26bf660b1e899036 Mon Sep 17 00:00:00 2001 From: Mehant Kammakomati Date: Thu, 13 Oct 2022 17:11:08 +0530 Subject: [PATCH] feat: add validator and retries to qa engine Signed-off-by: Mehant Kammakomati --- qaengine/cliengine.go | 43 +++++++++++++++++---- qaengine/defaultengine.go | 13 ++++++- qaengine/engine.go | 8 ---- qaengine/httprestengine.go | 15 ++++++- qaengine/storeengine.go | 14 ++++++- transformer/external/starlarktransformer.go | 33 ++++++++-------- types/qaengine/commonqa/commonqa.go | 11 +++++- 7 files changed, 101 insertions(+), 36 deletions(-) diff --git a/qaengine/cliengine.go b/qaengine/cliengine.go index 999882128..962fc4087 100644 --- a/qaengine/cliengine.go +++ b/qaengine/cliengine.go @@ -81,9 +81,14 @@ func (*CliEngine) fetchSelectAnswer(prob qatypes.Problem) (qatypes.Problem, erro Options: prob.Options, Default: def, } - if err := survey.AskOne(prompt, &ans); err != nil { + question := &survey.Question{ + Prompt: prompt, + Validate: prob.Validator, + } + if err := survey.Ask([]*survey.Question{question}, &ans); err != nil { logrus.Fatalf("Error while asking a question : %s", err) } + prob.Answer = ans return prob, nil } @@ -95,8 +100,12 @@ func (*CliEngine) fetchMultiSelectAnswer(prob qatypes.Problem) (qatypes.Problem, Options: prob.Options, Default: prob.Default, } + question := &survey.Question{ + Prompt: prompt, + Validate: prob.Validator, + } tickIcon := func(icons *survey.IconSet) { icons.MarkedOption.Text = "[\u2713]" } - if err := survey.AskOne(prompt, &ans, survey.WithIcons(tickIcon)); err != nil { + if err := survey.Ask([]*survey.Question{question}, &ans, survey.WithIcons(tickIcon)); err != nil { logrus.Fatalf("Error while asking a question : %s", err) } otherAnsPresent := false @@ -114,7 +123,11 @@ func (*CliEngine) fetchMultiSelectAnswer(prob qatypes.Problem) (qatypes.Problem, Message: getQAMessage(prob), Default: "", } - if err := survey.AskOne(prompt, &multilineAns); err != nil { + question := &survey.Question{ + Prompt: prompt, + Validate: prob.Validator, + } + if err := survey.Ask([]*survey.Question{question}, &multilineAns); err != nil { logrus.Fatalf("Error while asking a question : %s", err) } for _, lineAns := range strings.Split(multilineAns, "\n") { @@ -137,7 +150,11 @@ func (*CliEngine) fetchConfirmAnswer(prob qatypes.Problem) (qatypes.Problem, err Message: getQAMessage(prob), Default: def, } - if err := survey.AskOne(prompt, &ans); err != nil { + question := &survey.Question{ + Prompt: prompt, + Validate: prob.Validator, + } + if err := survey.Ask([]*survey.Question{question}, &ans); err != nil { logrus.Fatalf("Error while asking a question : %s", err) } prob.Answer = ans @@ -153,7 +170,11 @@ func (*CliEngine) fetchInputAnswer(prob qatypes.Problem) (qatypes.Problem, error Message: getQAMessage(prob), Default: def, } - if err := survey.AskOne(prompt, &ans); err != nil { + question := &survey.Question{ + Prompt: prompt, + Validate: prob.Validator, + } + if err := survey.Ask([]*survey.Question{question}, &ans); err != nil { logrus.Fatalf("Error while asking a question : %s", err) } prob.Answer = ans @@ -169,7 +190,11 @@ func (*CliEngine) fetchMultilineInputAnswer(prob qatypes.Problem) (qatypes.Probl Message: getQAMessage(prob), Default: def, } - if err := survey.AskOne(prompt, &ans); err != nil { + question := &survey.Question{ + Prompt: prompt, + Validate: prob.Validator, + } + if err := survey.Ask([]*survey.Question{question}, &ans); err != nil { logrus.Fatalf("Error while asking a question : %s", err) } prob.Answer = ans @@ -181,7 +206,11 @@ func (*CliEngine) fetchPasswordAnswer(prob qatypes.Problem) (qatypes.Problem, er prompt := &survey.Password{ Message: getQAMessage(prob), } - if err := survey.AskOne(prompt, &ans); err != nil { + question := &survey.Question{ + Prompt: prompt, + Validate: prob.Validator, + } + if err := survey.Ask([]*survey.Question{question}, &ans); err != nil { logrus.Fatalf("Error while asking a question : %s", err) } prob.Answer = ans diff --git a/qaengine/defaultengine.go b/qaengine/defaultengine.go index fa21ef877..34ddef463 100644 --- a/qaengine/defaultengine.go +++ b/qaengine/defaultengine.go @@ -17,6 +17,8 @@ package qaengine import ( + "fmt" + qatypes "github.com/konveyor/move2kube/types/qaengine" ) @@ -42,5 +44,14 @@ func (*DefaultEngine) IsInteractiveEngine() bool { // FetchAnswer fetches the default answers func (*DefaultEngine) FetchAnswer(prob qatypes.Problem) (qatypes.Problem, error) { err := prob.SetAnswer(prob.Default) - return prob, err + if err != nil { + return prob, err + } + if prob.Validator != nil { + err := prob.Validator(prob.Answer) + if err != nil { + return prob, fmt.Errorf("default value is invalid. Error : %s", err) + } + } + return prob, nil } diff --git a/qaengine/engine.go b/qaengine/engine.go index 020b523c0..766ea94ab 100644 --- a/qaengine/engine.go +++ b/qaengine/engine.go @@ -151,14 +151,6 @@ func FetchAnswer(prob qatypes.Problem) (qatypes.Problem, error) { } } } - if prob.Validator != nil { - err := prob.Validator(prob.Answer) - if err != nil { - logrus.Errorf("incorrect input. Error : %s", err) - prob.Answer = nil - return FetchAnswer(prob) - } - } for _, writeStore := range writeStores { writeStore.AddSolution(prob) } diff --git a/qaengine/httprestengine.go b/qaengine/httprestengine.go index c10052a6c..440a032ea 100644 --- a/qaengine/httprestengine.go +++ b/qaengine/httprestengine.go @@ -99,13 +99,14 @@ func (h *HTTPRESTEngine) FetchAnswer(prob qatypes.Problem) (qatypes.Problem, err logrus.Errorf("the QA problem object is invalid. Error: %q", err) return prob, err } - if prob.Answer == nil { + for prob.Answer == nil { logrus.Debugf("Passing problem to HTTP REST QA Engine ID: %s, desc: %s", prob.ID, prob.Desc) h.problemChan <- prob prob = <-h.answerChan if prob.Answer == nil { return prob, fmt.Errorf("failed to resolve the QA problem: %+v", prob) - } else if prob.Type == qatypes.MultiSelectSolutionFormType { + } + if prob.Type == qatypes.MultiSelectSolutionFormType { otherAnsPresent := false ans, err := common.ConvertInterfaceToSliceOfStrings(prob.Answer) if err != nil { @@ -137,7 +138,17 @@ func (h *HTTPRESTEngine) FetchAnswer(prob qatypes.Problem) (qatypes.Problem, err } prob.Answer = newAns } + if prob.Validator == nil { + break + } + err := prob.Validator(prob.Answer) + if err == nil { + break + } + logrus.Errorf("incorrect input. Error : %s", err) + prob.Answer = nil } + return prob, nil } diff --git a/qaengine/storeengine.go b/qaengine/storeengine.go index 9353336b0..5d66d54b4 100644 --- a/qaengine/storeengine.go +++ b/qaengine/storeengine.go @@ -17,6 +17,8 @@ package qaengine import ( + "fmt" + qatypes "github.com/konveyor/move2kube/types/qaengine" ) @@ -32,7 +34,17 @@ func (se *StoreEngine) StartEngine() error { // FetchAnswer fetches the answer from the store func (se *StoreEngine) FetchAnswer(prob qatypes.Problem) (qatypes.Problem, error) { - return se.store.GetSolution(prob) + problem, err := se.store.GetSolution(prob) + if err != nil { + return problem, err + } + if problem.Validator != nil { + err := problem.Validator(problem.Answer) + if err != nil { + return problem, fmt.Errorf("incorrect input. Error : %s", err) + } + } + return problem, nil } // IsInteractiveEngine returns true if the engine interacts with the user diff --git a/transformer/external/starlarktransformer.go b/transformer/external/starlarktransformer.go index 38eefd251..aa058dcde 100644 --- a/transformer/external/starlarktransformer.go +++ b/transformer/external/starlarktransformer.go @@ -36,6 +36,7 @@ import ( "github.com/qri-io/starlib" starutil "github.com/qri-io/starlib/util" "github.com/sirupsen/logrus" + "github.com/spf13/cast" "go.starlark.net/starlark" "go.starlark.net/starlarkstruct" ) @@ -275,28 +276,28 @@ func (t *Starlark) getStarlarkQuery() *starlark.Builtin { prob.Type = qatypes.InputSolutionFormType } if validation != "" { + validationFn, ok := t.StarGlobals[validation] + if !ok { + return starlark.None, fmt.Errorf("provided validation function not found : %s", validation) + } + fn, ok := validationFn.(*starlark.Function) + if !ok { + return starlark.None, fmt.Errorf("%s is not a function", validationFn) + } prob.Validator = func(ans interface{}) error { - validationFn := t.StarGlobals[validation] - fn, ok := validationFn.(*starlark.Function) - if !ok { - err = fmt.Errorf("%s is not a function", validationFn) - logrus.Errorf("%s", err) - } answer, err := starutil.Marshal(ans) if err != nil { - logrus.Errorf("unable to convert %s to starlark value : %s", ans, err) - return err + return fmt.Errorf("unable to convert %s to starlark value : %s", ans, err) } val, err := starlark.Call(t.StarThread, fn, starlark.Tuple{answer}, nil) if err != nil { - logrus.Errorf("Unable to execute the starlark function: Error : %s Value : %s", err, val) - return err + return fmt.Errorf("unable to execute the starlark function: Error : %s", err) } value, err := starutil.Unmarshal(val) if err != nil { - logrus.Errorf("Unable to unmarshal starlark function result : %s", err) - return err + return fmt.Errorf("unable to unmarshal starlark function result : %s", err) } + // if empty string is returned then we assume validation is successful if value.(string) != "" { return fmt.Errorf("validation failed : %s", value.(string)) } @@ -521,13 +522,13 @@ func (t *Starlark) getStarlarkFindXmlPath() *starlark.Builtin { } data := expr.Evaluate(xmlquery.CreateXPathNavigator(doc)) var result []interface{} - switch data.(type) { + switch d := data.(type) { case bool: - result = append(result, strconv.FormatBool(data.(bool))) + result = append(result, cast.ToString(d)) case float64: - result = append(result, strconv.FormatFloat(data.(float64), 'E', -1, 64)) + result = append(result, strconv.FormatFloat(d, 'E', -1, 64)) case string: - result = append(result, data.(string)) + result = append(result, d) case *xpath.NodeIterator: iterator := data.(*xpath.NodeIterator) for iterator.MoveNext() { diff --git a/types/qaengine/commonqa/commonqa.go b/types/qaengine/commonqa/commonqa.go index 5472d3dda..3e4aaf95b 100644 --- a/types/qaengine/commonqa/commonqa.go +++ b/types/qaengine/commonqa/commonqa.go @@ -78,7 +78,16 @@ func IngressHost(defaulthost string, clusterQaLabel string) string { // MinimumReplicaCount returns minimum replica count func MinimumReplicaCount(defaultminreplicas string) string { - return qaengine.FetchStringAnswer(common.ConfigMinReplicasKey, "Provide the minimum number of replicas each service should have", []string{"If the value is 0 pods won't be started by default"}, defaultminreplicas, nil) + return qaengine.FetchStringAnswer(common.ConfigMinReplicasKey, "Provide the minimum number of replicas each service should have", []string{"If the value is 0 pods won't be started by default"}, defaultminreplicas, func(replicaCount interface{}) error { + replicaCountI, err := cast.ToIntE(replicaCount) + if err != nil { + return err + } + if replicaCountI < 0 { + return fmt.Errorf("replica count should be a positive number") + } + return nil + }) } // GetPortsForService returns ports used by a service