Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add validator and retries to qa engine #898

Merged
merged 4 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
10 changes: 5 additions & 5 deletions common/sshkeys/sshkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func LoadKnownHostsOfCurrentUser() {
Move2Kube has public keys for github.com, gitlab.com, and bitbucket.org by default.
If any of the repos use ssh authentication we will need public keys in order to verify.
Do you want to load the public keys from your [%s]?:`
ans := qaengine.FetchBoolAnswer(common.ConfigRepoLoadPubKey, fmt.Sprintf(message, knownHostsPath), []string{"No, I will add them later if necessary."}, false)
ans := qaengine.FetchBoolAnswer(common.ConfigRepoLoadPubKey, fmt.Sprintf(message, knownHostsPath), []string{"No, I will add them later if necessary."}, false, nil)
if !ans {
logrus.Debug("Don't read public keys from known_hosts. They will be added later if necessary.")
return
Expand Down Expand Up @@ -105,7 +105,7 @@ func loadSSHKeysOfCurrentUser() {
message := `The CI/CD pipeline needs access to the git repos in order to clone, build and push.
If any of the repos require ssh keys you will need to provide them.
Do you want to load the private ssh keys from [%s]?:`
ans := qaengine.FetchBoolAnswer(common.ConfigRepoLoadPrivKey, fmt.Sprintf(message, privateKeyDir), []string{"No, I will add them later if necessary."}, false)
ans := qaengine.FetchBoolAnswer(common.ConfigRepoLoadPrivKey, fmt.Sprintf(message, privateKeyDir), []string{"No, I will add them later if necessary."}, false, nil)
if !ans {
logrus.Debug("Don't read private keys. They will be added later if necessary.")
return
Expand All @@ -125,7 +125,7 @@ Do you want to load the private ssh keys from [%s]?:`
for _, finfo := range finfos {
filenames = append(filenames, finfo.Name())
}
filenames = qaengine.FetchMultiSelectAnswer(common.ConfigRepoKeyPathsKey, fmt.Sprintf("These are the files we found in %q . Which keys should we consider?", privateKeyDir), []string{"Select all the keys that give access to git repos."}, filenames, filenames)
filenames = qaengine.FetchMultiSelectAnswer(common.ConfigRepoKeyPathsKey, fmt.Sprintf("These are the files we found in %q . Which keys should we consider?", privateKeyDir), []string{"Select all the keys that give access to git repos."}, filenames, filenames, nil)
if len(filenames) == 0 {
logrus.Info("All key files ignored.")
return
Expand Down Expand Up @@ -170,7 +170,7 @@ func loadSSHKey(filename string) (string, error) {
qaKey := common.JoinQASubKeys(common.ConfigRepoPrivKey, `"`+filename+`"`, "password")
desc := fmt.Sprintf("Enter the password to decrypt the private key %q : ", filename)
hints := []string{"Password:"}
password := qaengine.FetchPasswordAnswer(qaKey, desc, hints)
password := qaengine.FetchPasswordAnswer(qaKey, desc, hints, nil)
key, err = ssh.ParseRawPrivateKeyWithPassphrase(fileBytes, []byte(password))
if err != nil {
logrus.Errorf("Failed to parse the encrypted private key file at path %q Error %q", path, err)
Expand Down Expand Up @@ -202,7 +202,7 @@ func GetSSHKey(domain string) (string, bool) {
qaKey := common.JoinQASubKeys(common.ConfigRepoKeysKey, `"`+domain+`"`, "key")
desc := fmt.Sprintf("Select the key to use for the git domain %s :", domain)
hints := []string{fmt.Sprintf("If none of the keys are correct, select %s", noAnswer)}
filename := qaengine.FetchSelectAnswer(qaKey, desc, hints, noAnswer, filenames)
filename := qaengine.FetchSelectAnswer(qaKey, desc, hints, noAnswer, filenames, nil)
if filename == noAnswer {
logrus.Debugf("No key selected for domain %s", domain)
return "", false
Expand Down
2 changes: 1 addition & 1 deletion environment/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func initContainerEngine() (err error) {
// GetContainerEngine gets a working container engine
func GetContainerEngine() ContainerEngine {
if !inited {
disabled = !qaengine.FetchBoolAnswer(common.ConfigSpawnContainersKey, "Allow spawning containers?", []string{"If this setting is set to false, those transformers that rely on containers will not work."}, false)
disabled = !qaengine.FetchBoolAnswer(common.ConfigSpawnContainersKey, "Allow spawning containers?", []string{"If this setting is set to false, those transformers that rely on containers will not work."}, false, nil)
if !disabled {
if err := initContainerEngine(); err != nil {
logrus.Fatalf("failed to initialize the container engine. Error: %q", err)
Expand Down
2 changes: 1 addition & 1 deletion lib/transformer.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func Transform(ctx context.Context, plan plantypes.Plan, outputPath string, tran
serviceNames = append(serviceNames, serviceName)
}
sort.Strings(serviceNames)
selectedServiceNames := qaengine.FetchMultiSelectAnswer(common.ConfigServicesNamesKey, "Select all services that are needed:", []string{"The services unselected here will be ignored."}, serviceNames, serviceNames)
selectedServiceNames := qaengine.FetchMultiSelectAnswer(common.ConfigServicesNamesKey, "Select all services that are needed:", []string{"The services unselected here will be ignored."}, serviceNames, serviceNames, nil)

// select the first valid transformation option for each selected service
selectedTransformationOptions := []plantypes.PlanArtifact{}
Expand Down
43 changes: 36 additions & 7 deletions qaengine/cliengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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") {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
13 changes: 12 additions & 1 deletion qaengine/defaultengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package qaengine

import (
"fmt"

qatypes "github.com/konveyor/move2kube/types/qaengine"
)

Expand All @@ -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
}
10 changes: 5 additions & 5 deletions qaengine/defaultengine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestDefaultEngine(t *testing.T) {
AddEngine(e)
defaultRegistryURL := "quay.io"
key := common.JoinQASubKeys(common.BaseKey, "input")
answer := FetchStringAnswer(key, "Enter the name of the registry : ", []string{"Ex : " + defaultRegistryURL}, defaultRegistryURL)
answer := FetchStringAnswer(key, "Enter the name of the registry : ", []string{"Ex : " + defaultRegistryURL}, defaultRegistryURL, nil)
if answer != defaultRegistryURL {
t.Fatalf("Fetched answer was different from the default one. Fetched answer: %s, expected answer: %s ",
answer, defaultRegistryURL)
Expand All @@ -54,7 +54,7 @@ func TestDefaultEngine(t *testing.T) {
def := "Option B"
opts := []string{"Option A", "Option B", "Option C"}

answer := FetchSelectAnswer(key, desc, context, def, opts)
answer := FetchSelectAnswer(key, desc, context, def, opts, nil)
if answer != def {
t.Fatalf("Fetched answer was different from the default one. Fetched answer: %s, expected answer: %s ",
answer, def)
Expand All @@ -74,7 +74,7 @@ func TestDefaultEngine(t *testing.T) {
def := []string{"Option A", "Option C"}
opts := []string{"Option A", "Option B", "Option C", "Option D"}

answer := FetchMultiSelectAnswer(key, desc, context, def, opts)
answer := FetchMultiSelectAnswer(key, desc, context, def, opts, nil)
if !cmp.Equal(answer, def) {
t.Fatalf("Fetched answer was different from the default one. Fetched answer: %s, expected answer: %s ",
answer, def)
Expand All @@ -93,7 +93,7 @@ func TestDefaultEngine(t *testing.T) {
context := []string{"Test context"}
def := true

answer := FetchBoolAnswer(key, desc, context, def)
answer := FetchBoolAnswer(key, desc, context, def, nil)
if answer != def {
t.Fatalf("Fetched answer was different from the default one. Fetched answer: %v, expected answer: %v ",
answer, def)
Expand All @@ -114,7 +114,7 @@ func TestDefaultEngine(t *testing.T) {
line2
line3`

answer := FetchMultilineInputAnswer(key, desc, context, def)
answer := FetchMultilineInputAnswer(key, desc, context, def, nil)
if answer != def {
t.Fatalf("Fetched answer was different from the default one. Fetched answer: %s, expected answer: %s ",
answer, def)
Expand Down
30 changes: 17 additions & 13 deletions qaengine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ func FetchAnswer(prob qatypes.Problem) (qatypes.Problem, error) {
}
prob, err = e.FetchAnswer(prob)
if err != nil {
if _, ok := err.(*qatypes.ValidationError); ok {
logrus.Errorf("Error while fetching answer using engine %T Error: %q", e, err)
continue
}
logrus.Debugf("Error while fetching answer using engine %T Error: %q", e, err)
continue
}
Expand Down Expand Up @@ -176,7 +180,7 @@ func WriteStoresToDisk() error {
func changeSelectToInputForOther(prob qatypes.Problem) qatypes.Problem {
if prob.Type == qatypes.SelectSolutionFormType && prob.Answer != nil && prob.Answer.(string) == qatypes.OtherAnswer {
newDesc := string(qatypes.InputSolutionFormType) + " " + prob.Desc
newProb, err := qatypes.NewInputProblem(prob.ID, newDesc, nil, "")
newProb, err := qatypes.NewInputProblem(prob.ID, newDesc, nil, "", prob.Validator)
if err != nil {
logrus.Fatalf("failed to change the QA select type problem to input type problem: %+v\nError: %q", prob, err)
}
Expand All @@ -188,8 +192,8 @@ func changeSelectToInputForOther(prob qatypes.Problem) qatypes.Problem {
// Convenience functions

// FetchStringAnswer asks a input type question and gets a string as the answer
func FetchStringAnswer(probid, desc string, context []string, def string) string {
problem, err := qatypes.NewInputProblem(probid, desc, context, def)
func FetchStringAnswer(probid, desc string, context []string, def string, validator func(interface{}) error) string {
problem, err := qatypes.NewInputProblem(probid, desc, context, def, validator)
if err != nil {
logrus.Fatalf("Unable to create problem. Error: %q", err)
}
Expand All @@ -205,8 +209,8 @@ func FetchStringAnswer(probid, desc string, context []string, def string) string
}

// FetchBoolAnswer asks a confirm type question and gets a boolean as the answer
func FetchBoolAnswer(probid, desc string, context []string, def bool) bool {
problem, err := qatypes.NewConfirmProblem(probid, desc, context, def)
func FetchBoolAnswer(probid, desc string, context []string, def bool, validator func(interface{}) error) bool {
problem, err := qatypes.NewConfirmProblem(probid, desc, context, def, validator)
if err != nil {
logrus.Fatalf("Unable to create problem. Error: %q", err)
}
Expand All @@ -222,8 +226,8 @@ func FetchBoolAnswer(probid, desc string, context []string, def bool) bool {
}

// FetchSelectAnswer asks a select type question and gets a string as the answer
func FetchSelectAnswer(probid, desc string, context []string, def string, options []string) string {
problem, err := qatypes.NewSelectProblem(probid, desc, context, def, options)
func FetchSelectAnswer(probid, desc string, context []string, def string, options []string, validator func(interface{}) error) string {
problem, err := qatypes.NewSelectProblem(probid, desc, context, def, options, validator)
if err != nil {
logrus.Fatalf("Unable to create problem. Error: %q", err)
}
Expand All @@ -239,8 +243,8 @@ func FetchSelectAnswer(probid, desc string, context []string, def string, option
}

// FetchMultiSelectAnswer asks a multi-select type question and gets a slice of strings as the answer
func FetchMultiSelectAnswer(probid, desc string, context, def, options []string) []string {
problem, err := qatypes.NewMultiSelectProblem(probid, desc, context, def, options)
func FetchMultiSelectAnswer(probid, desc string, context, def, options []string, validator func(interface{}) error) []string {
problem, err := qatypes.NewMultiSelectProblem(probid, desc, context, def, options, validator)
if err != nil {
logrus.Fatalf("Unable to create problem. Error: %q", err)
}
Expand All @@ -256,8 +260,8 @@ func FetchMultiSelectAnswer(probid, desc string, context, def, options []string)
}

// FetchPasswordAnswer asks a password type question and gets a string as the answer
func FetchPasswordAnswer(probid, desc string, context []string) string {
problem, err := qatypes.NewPasswordProblem(probid, desc, context)
func FetchPasswordAnswer(probid, desc string, context []string, validator func(interface{}) error) string {
problem, err := qatypes.NewPasswordProblem(probid, desc, context, validator)
if err != nil {
logrus.Fatalf("Unable to create problem. Error: %q", err)
}
Expand All @@ -273,8 +277,8 @@ func FetchPasswordAnswer(probid, desc string, context []string) string {
}

// FetchMultilineInputAnswer asks a multi-line type question and gets a string as the answer
func FetchMultilineInputAnswer(probid, desc string, context []string, def string) string {
problem, err := qatypes.NewMultilineInputProblem(probid, desc, context, def)
func FetchMultilineInputAnswer(probid, desc string, context []string, def string, validator func(interface{}) error) string {
problem, err := qatypes.NewMultilineInputProblem(probid, desc, context, def, validator)
if err != nil {
logrus.Fatalf("Unable to create problem. Error: %q", err)
}
Expand Down
15 changes: 13 additions & 2 deletions qaengine/httprestengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down
12 changes: 11 additions & 1 deletion qaengine/storeengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,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, &qatypes.ValidationError{Reason: err.Error()}
}
}
return problem, nil
}

// IsInteractiveEngine returns true if the engine interacts with the user
Expand Down
Loading