diff --git a/common/sshkeys/sshkeys.go b/common/sshkeys/sshkeys.go index 95b4c42ba..4dfec79ed 100644 --- a/common/sshkeys/sshkeys.go +++ b/common/sshkeys/sshkeys.go @@ -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 @@ -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 @@ -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 @@ -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) @@ -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 diff --git a/environment/container/container.go b/environment/container/container.go index 03077decb..78ef4cdc0 100644 --- a/environment/container/container.go +++ b/environment/container/container.go @@ -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) diff --git a/lib/transformer.go b/lib/transformer.go index bd7cc0173..289b578c2 100644 --- a/lib/transformer.go +++ b/lib/transformer.go @@ -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{} 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/defaultengine_test.go b/qaengine/defaultengine_test.go index 31db3a477..383a7f425 100644 --- a/qaengine/defaultengine_test.go +++ b/qaengine/defaultengine_test.go @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/qaengine/engine.go b/qaengine/engine.go index 1ddc98574..79d45560a 100644 --- a/qaengine/engine.go +++ b/qaengine/engine.go @@ -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 } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } 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..34842ec5f 100644 --- a/qaengine/storeengine.go +++ b/qaengine/storeengine.go @@ -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 diff --git a/qaengine/storeengine_test.go b/qaengine/storeengine_test.go index 287b26972..9ccc3652b 100644 --- a/qaengine/storeengine_test.go +++ b/qaengine/storeengine_test.go @@ -44,7 +44,7 @@ func TestCacheEngine(t *testing.T) { want := "testuser" - answer := FetchStringAnswer(key, desc, context, def) + answer := FetchStringAnswer(key, desc, context, def, nil) if answer != want { t.Fatalf("Fetched answer was different from the default one. Fetched answer: %s, expected answer: %s ", answer, want) @@ -66,7 +66,7 @@ func TestCacheEngine(t *testing.T) { opts := []string{"Use existing pull secret", "No authentication", "UserName/Password"} want := "UserName/Password" - answer := FetchSelectAnswer(key, desc, context, def, opts) + answer := FetchSelectAnswer(key, desc, context, def, opts, nil) if answer != want { t.Fatalf("Fetched answer was different from the default one. Fetched answer: %s, expected answer: %s ", answer, want) @@ -89,7 +89,7 @@ line2 line3 ` - answer := FetchMultilineInputAnswer(key, desc, context, "") + answer := FetchMultilineInputAnswer(key, desc, context, "", nil) if answer != cachedAnswer { t.Fatalf("Fetched answer was different from the default one. Fetched answer: %s, expected answer: %s ", answer, cachedAnswer) @@ -110,7 +110,7 @@ line3 def := true want := true - answer := FetchBoolAnswer(key, desc, context, def) + answer := FetchBoolAnswer(key, desc, context, def, nil) if answer != want { t.Fatalf("Fetched answer was different from the default one. Fetched answer: %v, expected answer: %v ", answer, want) @@ -131,7 +131,7 @@ line3 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) diff --git a/transformer/cloudfoundrytransformer.go b/transformer/cloudfoundrytransformer.go index b1f1acc75..58938fdcc 100644 --- a/transformer/cloudfoundrytransformer.go +++ b/transformer/cloudfoundrytransformer.go @@ -225,6 +225,7 @@ func (t *CloudFoundry) Transform(newArtifacts []transformertypes.Artifact, alrea nil, []string{containerizationOptionsConfig[0]}, containerizationOptionsConfig, + nil, ) secondaryArtifactsGenerated := false for _, containerizationOption := range containerizationOptions { diff --git a/transformer/dockerfilegenerator/dotnet/utils.go b/transformer/dockerfilegenerator/dotnet/utils.go index 990f9cda5..93afbe248 100644 --- a/transformer/dockerfilegenerator/dotnet/utils.go +++ b/transformer/dockerfilegenerator/dotnet/utils.go @@ -55,7 +55,7 @@ func AskUserForDockerfileType(rootProjectName string) (buildOption, error) { fmt.Sprintf("[%s] Put the build stage in a separate Dockerfile and create a base image.", BUILD_IN_BASE_IMAGE), fmt.Sprintf("[%s] Put the build stage in every Dockerfile to make it self contained. (Warning: This may cause one build per Dockerfile.)", BUILD_IN_EVERY_IMAGE), } - selectedBuildOption := buildOption(qaengine.FetchSelectAnswer(quesId, desc, hints, string(def), options)) + selectedBuildOption := buildOption(qaengine.FetchSelectAnswer(quesId, desc, hints, string(def), options, nil)) switch selectedBuildOption { case NO_BUILD_STAGE, BUILD_IN_BASE_IMAGE, BUILD_IN_EVERY_IMAGE: return selectedBuildOption, nil diff --git a/transformer/dockerfilegenerator/dotnetcoredockerfilegenerator.go b/transformer/dockerfilegenerator/dotnetcoredockerfilegenerator.go index da8b1fcfc..740569af4 100644 --- a/transformer/dockerfilegenerator/dotnetcoredockerfilegenerator.go +++ b/transformer/dockerfilegenerator/dotnetcoredockerfilegenerator.go @@ -286,7 +286,7 @@ func (t *DotNetCoreDockerfileGenerator) TransformArtifact(newArtifact transforme quesKey := fmt.Sprintf(common.ConfigServicesDotNetChildProjectsNamesKey, `"`+newArtifact.Name+`"`) desc := fmt.Sprintf("For the multi-project Dot Net Core app '%s', please select all the child projects that should be run as services in the cluster:", newArtifact.Name) hints := []string{"deselect any child project that should not be run (example: libraries)"} - selectedChildProjectNames = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, selectedChildProjectNames, selectedChildProjectNames) + selectedChildProjectNames = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, selectedChildProjectNames, selectedChildProjectNames, nil) if len(selectedChildProjectNames) == 0 { return pathMappings, artifactsCreated, fmt.Errorf("user deselected all the child projects of the dot net core multi-project app '%s'", newArtifact.Name) } @@ -646,7 +646,7 @@ func getPublishProfile(profilePaths []string, subKey, baseDir string) (string, s if len(relProfilePaths) > 1 { quesKey := common.JoinQASubKeys(common.ConfigServicesKey, subKey, common.ConfigPublishProfileForServiceKeySegment) desc := fmt.Sprintf("Select the profile to be use for publishing the ASP.NET child project %s :", subKey) - relSelectedProfilePath = qaengine.FetchSelectAnswer(quesKey, desc, nil, relSelectedProfilePath, relProfilePaths) + relSelectedProfilePath = qaengine.FetchSelectAnswer(quesKey, desc, nil, relSelectedProfilePath, relProfilePaths, nil) } selectedProfilePath := filepath.Join(baseDir, relSelectedProfilePath) publishUrl, err := parsePublishProfileFile(selectedProfilePath) diff --git a/transformer/dockerfilegenerator/java/gradleanalyser.go b/transformer/dockerfilegenerator/java/gradleanalyser.go index 0cd6bcff2..4567cae0a 100644 --- a/transformer/dockerfilegenerator/java/gradleanalyser.go +++ b/transformer/dockerfilegenerator/java/gradleanalyser.go @@ -442,7 +442,7 @@ func (t *GradleAnalyser) TransformArtifact(newArtifact transformertypes.Artifact quesKey := fmt.Sprintf(common.ConfigServicesChildModulesNamesKey, `"`+serviceConfig.ServiceName+`"`) desc := fmt.Sprintf("For the multi-module Gradle project '%s', please select all the child modules that should be run as services in the cluster:", serviceConfig.ServiceName) hints := []string{"deselect child modules that should not be run (like libraries)"} - selectedChildModuleNames = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, selectedChildModuleNames, selectedChildModuleNames) + selectedChildModuleNames = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, selectedChildModuleNames, selectedChildModuleNames, nil) if len(selectedChildModuleNames) == 0 { return pathMappings, createdArtifacts, fmt.Errorf("user deselected all the child modules of the gradle multi-module project '%s'", serviceConfig.ServiceName) } @@ -495,7 +495,7 @@ func (t *GradleAnalyser) TransformArtifact(newArtifact transformertypes.Artifact if childModuleInfo.SpringBoot != nil { if childModuleInfo.SpringBoot.SpringBootProfiles != nil && len(*childModuleInfo.SpringBoot.SpringBootProfiles) != 0 { quesKey := fmt.Sprintf(common.ConfigServicesChildModulesSpringProfilesKey, `"`+serviceConfig.ServiceName+`"`, `"`+childModule.Name+`"`) - selectedSpringProfiles := qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, *childModuleInfo.SpringBoot.SpringBootProfiles, *childModuleInfo.SpringBoot.SpringBootProfiles) + selectedSpringProfiles := qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, *childModuleInfo.SpringBoot.SpringBootProfiles, *childModuleInfo.SpringBoot.SpringBootProfiles, nil) for _, selectedSpringProfile := range selectedSpringProfiles { detectedPorts = append(detectedPorts, childModuleInfo.SpringBoot.SpringBootProfilePorts[selectedSpringProfile]...) } diff --git a/transformer/dockerfilegenerator/java/mavenanalyser.go b/transformer/dockerfilegenerator/java/mavenanalyser.go index e846398de..a13141a41 100644 --- a/transformer/dockerfilegenerator/java/mavenanalyser.go +++ b/transformer/dockerfilegenerator/java/mavenanalyser.go @@ -278,7 +278,7 @@ func (t *MavenAnalyser) TransformArtifact(newArtifact transformertypes.Artifact, quesKey := fmt.Sprintf(common.ConfigServicesChildModulesNamesKey, `"`+serviceConfig.ServiceName+`"`) desc := fmt.Sprintf("For the multi-module Maven project '%s', please select all the child modules that should be run as services in the cluster:", serviceConfig.ServiceName) hints := []string{"deselect child modules that should not be run (like libraries)"} - selectedChildModuleNames = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, selectedChildModuleNames, selectedChildModuleNames) + selectedChildModuleNames = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, selectedChildModuleNames, selectedChildModuleNames, nil) if len(selectedChildModuleNames) == 0 { return pathMappings, createdArtifacts, fmt.Errorf("user deselected all the child modules of the maven multi-module project '%s'", serviceConfig.ServiceName) } @@ -331,7 +331,7 @@ func (t *MavenAnalyser) TransformArtifact(newArtifact transformertypes.Artifact, if childModuleInfo.SpringBoot != nil { if childModuleInfo.SpringBoot.SpringBootProfiles != nil && len(*childModuleInfo.SpringBoot.SpringBootProfiles) != 0 { quesKey := fmt.Sprintf(common.ConfigServicesChildModulesSpringProfilesKey, `"`+serviceConfig.ServiceName+`"`, `"`+childModule.Name+`"`) - selectedSpringProfiles := qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, *childModuleInfo.SpringBoot.SpringBootProfiles, *childModuleInfo.SpringBoot.SpringBootProfiles) + selectedSpringProfiles := qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, *childModuleInfo.SpringBoot.SpringBootProfiles, *childModuleInfo.SpringBoot.SpringBootProfiles, nil) for _, selectedSpringProfile := range selectedSpringProfiles { detectedPorts = append(detectedPorts, childModuleInfo.SpringBoot.SpringBootProfilePorts[selectedSpringProfile]...) } @@ -452,6 +452,7 @@ func (t *MavenAnalyser) TransformArtifact(newArtifact transformertypes.Artifact, []string{"The selected maven profiles will be used during the build."}, rootPomInfo.MavenProfiles, rootPomInfo.MavenProfiles, + nil, ) // fill in the Dockerfile template for the build stage and write it out using a pathmapping diff --git a/transformer/dockerfilegenerator/java/utils.go b/transformer/dockerfilegenerator/java/utils.go index 1f7ec5b15..79134e64b 100644 --- a/transformer/dockerfilegenerator/java/utils.go +++ b/transformer/dockerfilegenerator/java/utils.go @@ -70,7 +70,7 @@ func askUserForDockerfileType(rootProjectName string) (buildOption, error) { fmt.Sprintf("[%s] Put the build stage in a separate Dockerfile and create a base image.", BUILD_IN_BASE_IMAGE), fmt.Sprintf("[%s] Put the build stage in every Dockerfile to make it self contained. (Warning: This may cause one build per Dockerfile.)", BUILD_IN_EVERY_IMAGE), } - selectedBuildOption := buildOption(qaengine.FetchSelectAnswer(quesId, desc, hints, string(def), options)) + selectedBuildOption := buildOption(qaengine.FetchSelectAnswer(quesId, desc, hints, string(def), options, nil)) switch selectedBuildOption { case NO_BUILD_STAGE, BUILD_IN_BASE_IMAGE, BUILD_IN_EVERY_IMAGE: return selectedBuildOption, nil diff --git a/transformer/dockerfilegenerator/phpdockerfiletransformer.go b/transformer/dockerfilegenerator/phpdockerfiletransformer.go index 641db268d..673ef0f51 100644 --- a/transformer/dockerfilegenerator/phpdockerfiletransformer.go +++ b/transformer/dockerfilegenerator/phpdockerfiletransformer.go @@ -133,7 +133,7 @@ func GetConfFileForService(confFiles []string, serviceName string) string { quesKey := common.JoinQASubKeys(common.ConfigServicesKey, `"`+serviceName+`"`, common.ConfigApacheConfFileForServiceKeySegment) desc := fmt.Sprintf("Choose the apache config file to be used for the service %s", serviceName) hints := []string{fmt.Sprintf("Selected apache config file will be used for identifying the port to be exposed for the service %s", serviceName)} - selectedConfFile := qaengine.FetchSelectAnswer(quesKey, desc, hints, confFiles[0], confFiles) + selectedConfFile := qaengine.FetchSelectAnswer(quesKey, desc, hints, confFiles[0], confFiles, nil) if selectedConfFile == noAnswer { logrus.Debugf("No apache config file selected for the service %s", serviceName) return "" diff --git a/transformer/dockerfilegenerator/pythondockerfiletransformer.go b/transformer/dockerfilegenerator/pythondockerfiletransformer.go index f9dd7b264..5378d2981 100644 --- a/transformer/dockerfilegenerator/pythondockerfiletransformer.go +++ b/transformer/dockerfilegenerator/pythondockerfiletransformer.go @@ -130,7 +130,7 @@ func getMainPythonFileForService(mainPythonFilesPath []string, baseDir string, s quesKey := common.JoinQASubKeys(common.ConfigServicesKey, `"`+serviceName+`"`, common.ConfigMainPythonFileForServiceKeySegment) desc := fmt.Sprintf("Select the main file to be used for the service %s :", serviceName) hints := []string{fmt.Sprintf("Selected main file will be used for the service %s", serviceName)} - return qaengine.FetchSelectAnswer(quesKey, desc, hints, mainPythonFilesRelPath[0], mainPythonFilesRelPath) + return qaengine.FetchSelectAnswer(quesKey, desc, hints, mainPythonFilesRelPath[0], mainPythonFilesRelPath, nil) } // getStartingPythonFileForService returns the starting python file used by a service @@ -144,7 +144,7 @@ func getStartingPythonFileForService(pythonFilesPath []string, baseDir string, s quesKey := common.JoinQASubKeys(common.ConfigServicesKey, `"`+serviceName+`"`, common.ConfigStartingPythonFileForServiceKeySegment) desc := fmt.Sprintf("Select the python file to be used for the service %s :", serviceName) hints := []string{fmt.Sprintf("Selected python file will be used for starting the service %s", serviceName)} - return qaengine.FetchSelectAnswer(quesKey, desc, hints, pythonFilesRelPath[0], pythonFilesRelPath) + return qaengine.FetchSelectAnswer(quesKey, desc, hints, pythonFilesRelPath[0], pythonFilesRelPath, nil) } // DirectoryDetect runs detect in each sub directory diff --git a/transformer/dockerfilegenerator/windows/webappdockerfilegenerator.go b/transformer/dockerfilegenerator/windows/webappdockerfilegenerator.go index 2ece6a3eb..615448c96 100644 --- a/transformer/dockerfilegenerator/windows/webappdockerfilegenerator.go +++ b/transformer/dockerfilegenerator/windows/webappdockerfilegenerator.go @@ -263,7 +263,7 @@ func (t *WinWebAppDockerfileGenerator) Transform(newArtifacts []transformertypes quesKey := fmt.Sprintf(common.ConfigServicesDotNetChildProjectsNamesKey, `"`+newArtifact.Name+`"`) desc := fmt.Sprintf("For the multi-project Dot Net app '%s', please select all the child projects that should be run as services in the cluster:", newArtifact.Name) hints := []string{"deselect any child project that should not be run (example: libraries)"} - selectedChildProjectNames = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, selectedChildProjectNames, selectedChildProjectNames) + selectedChildProjectNames = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, selectedChildProjectNames, selectedChildProjectNames, nil) if len(selectedChildProjectNames) == 0 { return pathMappings, artifactsCreated, fmt.Errorf("user deselected all the child projects of the dot net multi-project app '%s'", newArtifact.Name) } diff --git a/transformer/external/starlarktransformer.go b/transformer/external/starlarktransformer.go index 16b9f4724..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" ) @@ -249,7 +250,8 @@ func (t *Starlark) executeDetect(fn *starlark.Function, dir string) (services ma func (t *Starlark) getStarlarkQuery() *starlark.Builtin { return starlark.NewBuiltin(qaFnName, func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { argDictValue := &starlark.Dict{} - if err := starlark.UnpackPositionalArgs(qaFnName, args, kwargs, 1, &argDictValue); err != nil { + var validation string + if err := starlark.UnpackPositionalArgs(qaFnName, args, kwargs, 1, &argDictValue, &validation); err != nil { return starlark.None, fmt.Errorf("invalid args provided to '%s'. Expected a single dict argument. Error: %q", qaFnName, err) } argI, err := starutil.Unmarshal(argDictValue) @@ -273,6 +275,36 @@ func (t *Starlark) getStarlarkQuery() *starlark.Builtin { if prob.Type == "" { 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 { + answer, err := starutil.Marshal(ans) + if err != nil { + 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 { + return fmt.Errorf("unable to execute the starlark function: Error : %s", err) + } + value, err := starutil.Unmarshal(val) + if err != nil { + 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)) + } + return nil + } + } + resolved, err := qaengine.FetchAnswer(prob) if err != nil { logrus.Fatalf("failed to ask the question. Error: %q", err) @@ -490,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/transformer/kubernetes/apiresource/service.go b/transformer/kubernetes/apiresource/service.go index 39f227806..5b4719aaf 100644 --- a/transformer/kubernetes/apiresource/service.go +++ b/transformer/kubernetes/apiresource/service.go @@ -369,7 +369,7 @@ func (d *Service) createIngress(ir irtypes.EnhancedIR, targetCluster collecttype // Set the default ingressClass value quesKeyClass := common.JoinQASubKeys(qaId, common.ConfigIngressClassNameKeySuffix) descClass := "Provide the Ingress class name for ingress" - ingressClassName := qaengine.FetchStringAnswer(quesKeyClass, descClass, []string{"Leave empty to use the cluster default"}, "") + ingressClassName := qaengine.FetchStringAnswer(quesKeyClass, descClass, []string{"Leave empty to use the cluster default"}, "", nil) // Configure the rule with the above fan-out paths rules := []networking.IngressRule{} @@ -381,7 +381,7 @@ func (d *Service) createIngress(ir irtypes.EnhancedIR, targetCluster collecttype } quesKeyTLS := common.JoinQASubKeys(qaId, common.ConfigIngressTLSKeySuffix) descTLS := "Provide the TLS secret for ingress" - secretName = qaengine.FetchStringAnswer(quesKeyTLS, descTLS, []string{"Leave empty to use http"}, defaultSecretName) + secretName = qaengine.FetchStringAnswer(quesKeyTLS, descTLS, []string{"Leave empty to use http"}, defaultSecretName, nil) for hostprefix, httpIngressPaths := range hostHTTPIngressPaths { ph := host if hostprefix != "" { diff --git a/transformer/kubernetes/clusterselector.go b/transformer/kubernetes/clusterselector.go index a24292708..25c1fe486 100644 --- a/transformer/kubernetes/clusterselector.go +++ b/transformer/kubernetes/clusterselector.go @@ -116,6 +116,7 @@ func (t *ClusterSelectorTransformer) Transform(newArtifacts []transformertypes.A common.JoinQASubKeys(common.ConfigTargetKey, `"`+t.CSConfig.ClusterQaLabel+`"`, clusterTypeKey), "Choose the cluster type:", []string{"Choose the cluster type you would like to target"}, def, clusterTypeList, + nil, ) for ai := range newArtifacts { if newArtifacts[ai].Configs == nil { diff --git a/transformer/kubernetes/irpreprocessor/ingresspreprocessor.go b/transformer/kubernetes/irpreprocessor/ingresspreprocessor.go index 067d27b44..c8aeb6ba1 100644 --- a/transformer/kubernetes/irpreprocessor/ingresspreprocessor.go +++ b/transformer/kubernetes/irpreprocessor/ingresspreprocessor.go @@ -47,7 +47,7 @@ func (opt *ingressPreprocessor) preprocess(ir irtypes.IR) (irtypes.IR, error) { desc := fmt.Sprintf("What kind of service/ingress should be created for the service %s's %d port?", serviceName, portForwarding.ServicePort.Number) hints := []string{"Choose " + common.IngressKind + " if you want a ingress/route resource to be created"} quesKey := common.JoinQASubKeys(portKeyPart, "servicetype") - portForwarding.ServiceType = core.ServiceType(qaengine.FetchSelectAnswer(quesKey, desc, hints, common.IngressKind, options)) + portForwarding.ServiceType = core.ServiceType(qaengine.FetchSelectAnswer(quesKey, desc, hints, common.IngressKind, options, nil)) if string(portForwarding.ServiceType) == noneServiceType { portForwarding.ServiceType = "" } @@ -55,7 +55,7 @@ func (opt *ingressPreprocessor) preprocess(ir irtypes.IR) (irtypes.IR, error) { desc := fmt.Sprintf("Specify the ingress path to expose the service %s's %d port on?", serviceName, portForwarding.ServicePort.Number) hints := []string{"Leave out leading / to use first part as subdomain"} quesKey := common.JoinQASubKeys(portKeyPart, "urlpath") - portForwarding.ServiceRelPath = strings.TrimSpace(qaengine.FetchStringAnswer(quesKey, desc, hints, portForwarding.ServiceRelPath)) + portForwarding.ServiceRelPath = strings.TrimSpace(qaengine.FetchStringAnswer(quesKey, desc, hints, portForwarding.ServiceRelPath, nil)) portForwarding.ServiceType = core.ServiceTypeClusterIP } else { portForwarding.ServiceRelPath = "" diff --git a/transformer/kubernetes/irpreprocessor/registrypreprocessor.go b/transformer/kubernetes/irpreprocessor/registrypreprocessor.go index dd7a1e5c3..d28d91deb 100644 --- a/transformer/kubernetes/irpreprocessor/registrypreprocessor.go +++ b/transformer/kubernetes/irpreprocessor/registrypreprocessor.go @@ -134,19 +134,19 @@ func (p registryPreProcessor) preprocess(ir irtypes.IR) (irtypes.IR, error) { quesKey := fmt.Sprintf(common.ConfigImageRegistryLoginTypeKey, `"`+registry+`"`) desc := fmt.Sprintf("[%s] What type of container registry login do you want to use?", registry) hints := []string{"Docker login from config mode, will use the default config from your local machine."} - auth := qaengine.FetchSelectAnswer(quesKey, desc, hints, string(defaultOption), authOptions) + auth := qaengine.FetchSelectAnswer(quesKey, desc, hints, string(defaultOption), authOptions, nil) switch registryLoginOption(auth) { case noLogin: regAuth.Auth = "" case existingPullSecretLogin: qaKey := fmt.Sprintf(common.ConfigImageRegistryPullSecretKey, `"`+registry+`"`) - ps := qaengine.FetchStringAnswer(qaKey, fmt.Sprintf("[%s] Enter the name of the pull secret : ", registry), []string{"The pull secret should exist in the namespace where you will be deploying the application."}, "") + ps := qaengine.FetchStringAnswer(qaKey, fmt.Sprintf("[%s] Enter the name of the pull secret : ", registry), []string{"The pull secret should exist in the namespace where you will be deploying the application."}, "", nil) imagePullSecrets[registry] = ps case usernamePasswordLogin: qaUsernameKey := fmt.Sprintf(common.ConfigImageRegistryUserNameKey, `"`+registry+`"`) - regAuth.Username = qaengine.FetchStringAnswer(qaUsernameKey, fmt.Sprintf("[%s] Enter the username to login into the registry : ", registry), nil, "iamapikey") + regAuth.Username = qaengine.FetchStringAnswer(qaUsernameKey, fmt.Sprintf("[%s] Enter the username to login into the registry : ", registry), nil, "iamapikey", nil) qaPasswordKey := fmt.Sprintf(common.ConfigImageRegistryPasswordKey, `"`+registry+`"`) - regAuth.Password = qaengine.FetchPasswordAnswer(qaPasswordKey, fmt.Sprintf("[%s] Enter the password to login into the registry : ", registry), nil) + regAuth.Password = qaengine.FetchPasswordAnswer(qaPasswordKey, fmt.Sprintf("[%s] Enter the password to login into the registry : ", registry), nil, nil) case dockerConfigLogin: logrus.Debugf("using the credentials from the docker config.json file") } diff --git a/transformer/kubernetes/tektontransformer.go b/transformer/kubernetes/tektontransformer.go index bb1b475a3..f03a79730 100644 --- a/transformer/kubernetes/tektontransformer.go +++ b/transformer/kubernetes/tektontransformer.go @@ -316,7 +316,7 @@ func (t *Tekton) createGitSecret(name, gitRepoDomain string) irtypes.Storage { problemDesc := fmt.Sprintf("Unable to find the public key for the domain %s from known_hosts, please enter it. If don't know the public key, just leave this empty and you will be able to add it later: ", gitRepoDomain) hints := []string{"Ex : " + sshkeys.DomainToPublicKeys["github.com"][0]} qaKey := common.JoinQASubKeys(common.ConfigRepoLoadPubDomainsKey, `"`+gitRepoDomain+`"`, "pubkey") - knownHosts = qaengine.FetchStringAnswer(qaKey, problemDesc, hints, knownHostsPlaceholder) + knownHosts = qaengine.FetchStringAnswer(qaKey, problemDesc, hints, knownHostsPlaceholder, nil) } if key, ok := sshkeys.GetSSHKey(gitRepoDomain); ok { diff --git a/transformer/routertransformer.go b/transformer/routertransformer.go index 40a77ed74..fcdd6fccb 100644 --- a/transformer/routertransformer.go +++ b/transformer/routertransformer.go @@ -109,7 +109,7 @@ func (t *Router) Transform(newArtifacts []transformertypes.Artifact, alreadySeen filledHints = append(filledHints, filledHint) } logrus.Debugf("Using %s router to route %s artifact between %+v", t.Config.Name, newArtifact.Type, transformerNames) - transformerName := qaengine.FetchSelectAnswer(filledID, filledDesc, filledHints, transformerNames[0], transformerNames) + transformerName := qaengine.FetchSelectAnswer(filledID, filledDesc, filledHints, transformerNames[0], transformerNames, nil) newArtifact.ProcessWith.MatchExpressions = []metav1.LabelSelectorRequirement{{ Key: transformertypes.LabelName, Operator: metav1.LabelSelectorOpIn, diff --git a/transformer/transformer.go b/transformer/transformer.go index a57c8157b..3b79b6771 100644 --- a/transformer/transformer.go +++ b/transformer/transformer.go @@ -178,6 +178,7 @@ func InitTransformers(transformerToInit map[string]string, selector labels.Selec "Specify a Kubernetes style selector to select only the transformers that you want to run.", []string{"Leave empty to select everything. This is the default."}, "", + nil, ) if transformerFilterString != "" { if transformerFilter, err := common.ConvertStringSelectorsToSelectors(transformerFilterString); err != nil { @@ -216,6 +217,7 @@ func InitTransformers(transformerToInit map[string]string, selector labels.Selec []string{"Services that don't support any of the transformer types you are interested in will be ignored."}, defaultSelectedTransformerNames, transformerNames, + nil, ) for _, t := range transformerNames { if !common.IsPresent(selectedTransformerNames, t) { diff --git a/types/qaengine/commonqa/commonqa.go b/types/qaengine/commonqa/commonqa.go index e8211b244..3e4aaf95b 100644 --- a/types/qaengine/commonqa/commonqa.go +++ b/types/qaengine/commonqa/commonqa.go @@ -62,23 +62,32 @@ func ImageRegistry() string { if defreg == "" { defreg = defaultRegistryURL } - return qaengine.FetchSelectAnswer(common.ConfigImageRegistryURLKey, "Enter the URL of the image registry where the new images should be pushed : ", []string{"You can always change it later by changing the yamls."}, defreg, registryList) + return qaengine.FetchSelectAnswer(common.ConfigImageRegistryURLKey, "Enter the URL of the image registry where the new images should be pushed : ", []string{"You can always change it later by changing the yamls."}, defreg, registryList, nil) } // ImageRegistryNamespace returns Image Registry Namespace func ImageRegistryNamespace() string { - return qaengine.FetchStringAnswer(common.ConfigImageRegistryNamespaceKey, "Enter the namespace where the new images should be pushed : ", []string{"Ex : " + common.ProjectName}, common.ProjectName) + return qaengine.FetchStringAnswer(common.ConfigImageRegistryNamespaceKey, "Enter the namespace where the new images should be pushed : ", []string{"Ex : " + common.ProjectName}, common.ProjectName, nil) } // IngressHost returns Ingress host func IngressHost(defaulthost string, clusterQaLabel string) string { key := common.JoinQASubKeys(common.ConfigTargetKey, `"`+clusterQaLabel+`"`, common.ConfigIngressHostKeySuffix) - return qaengine.FetchStringAnswer(key, "Provide the ingress host domain", []string{"Ingress host domain is part of service URL"}, defaulthost) + return qaengine.FetchStringAnswer(key, "Provide the ingress host domain", []string{"Ingress host domain is part of service URL"}, defaulthost, nil) } // 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) + 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 @@ -93,7 +102,7 @@ func GetPortsForService(detectedPorts []int32, qaSubKey string) []int32 { quesKey := common.JoinQASubKeys(common.ConfigServicesKey, qaSubKey, common.ConfigPortsForServiceKeySegment) desc := fmt.Sprintf("Select ports to be exposed for the service '%s' :", qaSubKey) hints := []string{"Select 'Other' if you want to add more ports"} - selectedPortsStr = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, detectedPortsStr, allDetectedPortsStr) + selectedPortsStr = qaengine.FetchMultiSelectAnswer(quesKey, desc, hints, detectedPortsStr, allDetectedPortsStr, nil) } for _, portStr := range selectedPortsStr { portStr = strings.TrimSpace(portStr) @@ -122,7 +131,7 @@ func GetPortForService(detectedPorts []int32, qaSubKey string) int32 { detectedPortStrs = append(detectedPortStrs, cast.ToString(common.DefaultServicePort)) } detectedPortStrs = append(detectedPortStrs, qatypes.OtherAnswer) - selectedPortStr := qaengine.FetchSelectAnswer(quesKey, desc, hints, detectedPortStrs[0], detectedPortStrs) + selectedPortStr := qaengine.FetchSelectAnswer(quesKey, desc, hints, detectedPortStrs[0], detectedPortStrs, nil) selectedPortStr = strings.TrimSpace(selectedPortStr) selectedPort, err := strconv.ParseInt(selectedPortStr, 10, 32) if err != nil { @@ -139,5 +148,5 @@ func GetPortForService(detectedPorts []int32, qaSubKey string) int32 { // GetContainerRuntime returns the container runtime func GetContainerRuntime() string { containerRuntimes := []string{"docker", "podman"} - return qaengine.FetchSelectAnswer(common.ConfigContainerRuntimeKey, "Select the container runtime to use :", []string{"The container runtime selected will be used in the buildimages and pushimages scripts"}, containerRuntimes[0], containerRuntimes) + return qaengine.FetchSelectAnswer(common.ConfigContainerRuntimeKey, "Select the container runtime to use :", []string{"The container runtime selected will be used in the buildimages and pushimages scripts"}, containerRuntimes[0], containerRuntimes, nil) } diff --git a/types/qaengine/problem.go b/types/qaengine/problem.go index d321a8c4f..d3924b56e 100644 --- a/types/qaengine/problem.go +++ b/types/qaengine/problem.go @@ -53,13 +53,14 @@ const ( // Problem defines the QA problem type Problem struct { - ID string `yaml:"id" json:"id"` - Type SolutionFormType `yaml:"type,omitempty" json:"type,omitempty"` - Desc string `yaml:"description,omitempty" json:"description,omitempty"` - Hints []string `yaml:"hints,omitempty" json:"hints,omitempty"` - Options []string `yaml:"options,omitempty" json:"options,omitempty"` - Default interface{} `yaml:"default,omitempty" json:"default,omitempty"` - Answer interface{} `yaml:"answer,omitempty" json:"answer,omitempty"` + ID string `yaml:"id" json:"id"` + Type SolutionFormType `yaml:"type,omitempty" json:"type,omitempty"` + Desc string `yaml:"description,omitempty" json:"description,omitempty"` + Hints []string `yaml:"hints,omitempty" json:"hints,omitempty"` + Options []string `yaml:"options,omitempty" json:"options,omitempty"` + Default interface{} `yaml:"default,omitempty" json:"default,omitempty"` + Answer interface{} `yaml:"answer,omitempty" json:"answer,omitempty"` + Validator func(interface{}) error `yaml:"-" json:"-"` } // NewProblem creates a new problem object from a GRPC problem @@ -69,14 +70,31 @@ func NewProblem(p *qagrpc.Problem) (prob Problem, err error) { logrus.Errorf("Unable to convert defaults : %s", err) return prob, err } - return Problem{ + pp := Problem{ ID: p.Id, Type: SolutionFormType(p.Type), Desc: p.Description, Hints: p.Hints, Options: p.Options, Default: defaults, - }, nil + } + if p.Pattern != "" { + reg, err := regexp.Compile(p.Pattern) + if err != nil { + return pp, fmt.Errorf("not a valid regex pattern : Error : %s", err) + } + pp.Validator = func(ans interface{}) error { + a, ok := ans.(string) + if !ok { + return fmt.Errorf("expected input to be type String, got %T. Value : %+v", ans, ans) + } + if !reg.MatchString(a) { + return fmt.Errorf("pattern does not match : %s", a) + } + return nil + } + } + return pp, nil } // InterfaceToArray converts the answer interface to array @@ -196,87 +214,93 @@ func (p *Problem) matchString(str1 string, str2 string) bool { } // NewSelectProblem creates a new instance of select problem -func NewSelectProblem(probid, desc string, hints []string, def string, opts []string) (Problem, error) { +func NewSelectProblem(probid, desc string, hints []string, def string, opts []string, validator func(interface{}) error) (Problem, error) { var answer interface{} if len(opts) == 1 { answer = opts[0] } return Problem{ - ID: probid, - Desc: desc, - Hints: hints, - Type: SelectSolutionFormType, - Default: def, - Options: opts, - Answer: answer, + ID: probid, + Desc: desc, + Hints: hints, + Type: SelectSolutionFormType, + Default: def, + Options: opts, + Answer: answer, + Validator: validator, }, nil } // NewMultiSelectProblem creates a new instance of multiselect problem -func NewMultiSelectProblem(probid, desc string, hints []string, def []string, opts []string) (Problem, error) { +func NewMultiSelectProblem(probid, desc string, hints []string, def []string, opts []string, validator func(interface{}) error) (Problem, error) { var answer interface{} if len(opts) == 0 { answer = []string{} } return Problem{ - ID: probid, - Type: MultiSelectSolutionFormType, - Desc: desc, - Hints: hints, - Options: opts, - Default: def, - Answer: answer, + ID: probid, + Type: MultiSelectSolutionFormType, + Desc: desc, + Hints: hints, + Options: opts, + Default: def, + Answer: answer, + Validator: validator, }, nil } // NewConfirmProblem creates a new instance of confirm problem -func NewConfirmProblem(probid, desc string, hints []string, def bool) (Problem, error) { +func NewConfirmProblem(probid, desc string, hints []string, def bool, validator func(interface{}) error) (Problem, error) { return Problem{ - ID: probid, - Type: ConfirmSolutionFormType, - Desc: desc, - Hints: hints, - Options: nil, - Default: def, - Answer: nil, + ID: probid, + Type: ConfirmSolutionFormType, + Desc: desc, + Hints: hints, + Options: nil, + Default: def, + Answer: nil, + Validator: validator, }, nil } // NewInputProblem creates a new instance of input problem -func NewInputProblem(probid, desc string, hints []string, def string) (Problem, error) { +func NewInputProblem(probid, desc string, hints []string, def string, validator func(interface{}) error) (Problem, error) { return Problem{ - ID: probid, - Type: InputSolutionFormType, - Desc: desc, - Hints: hints, - Options: nil, - Default: def, - Answer: nil, + ID: probid, + Type: InputSolutionFormType, + Desc: desc, + Hints: hints, + Options: nil, + Default: def, + Answer: nil, + Validator: validator, }, nil } // NewMultilineInputProblem creates a new instance of multiline input problem -func NewMultilineInputProblem(probid, desc string, hints []string, def string) (Problem, error) { +func NewMultilineInputProblem(probid, desc string, hints []string, def string, validator func(interface{}) error) (Problem, error) { return Problem{ - ID: probid, - Type: MultilineInputSolutionFormType, - Desc: desc, - Hints: hints, - Options: nil, - Default: def, - Answer: nil, + ID: probid, + Type: MultilineInputSolutionFormType, + Desc: desc, + Hints: hints, + Options: nil, + Default: def, + Answer: nil, + Validator: validator, }, nil } // NewPasswordProblem creates a new instance of password problem -func NewPasswordProblem(probid, desc string, hints []string) (p Problem, err error) { +func NewPasswordProblem(probid, desc string, hints []string, validator func(interface{}) error) (p Problem, err error) { return Problem{ - ID: probid, - Type: PasswordSolutionFormType, - Desc: desc, - Hints: hints, - Options: nil, - Default: nil, - Answer: nil, + ID: probid, + Type: PasswordSolutionFormType, + Desc: desc, + Hints: hints, + Options: nil, + Default: nil, + Answer: nil, + Validator: validator, }, nil } diff --git a/types/qaengine/qaengine.go b/types/qaengine/qaengine.go index 4f7093801..4616881b1 100644 --- a/types/qaengine/qaengine.go +++ b/types/qaengine/qaengine.go @@ -19,6 +19,8 @@ Package qaengine contains the types used for the question answering part of the */ package qaengine +import "fmt" + // Store helps store answers type Store interface { Load() error @@ -27,3 +29,12 @@ type Store interface { Write() error AddSolution(p Problem) error } + +// ValidationError is the error while validating answer in QA Engine +type ValidationError struct { + Reason string +} + +func (v *ValidationError) Error() string { + return fmt.Sprintf("validation error: %s", v.Reason) +} diff --git a/types/qaengine/qagrpc/fetchanswer.pb.go b/types/qaengine/qagrpc/fetchanswer.pb.go index 988fc050c..18a61df7e 100644 --- a/types/qaengine/qagrpc/fetchanswer.pb.go +++ b/types/qaengine/qagrpc/fetchanswer.pb.go @@ -1,5 +1,5 @@ /* - * Copyright IBM Corporation 2020, 2021 + * Copyright IBM Corporation 2020, 2021, 2022 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,27 +14,13 @@ * limitations under the License. */ -// -//Copyright IBM Corporation 2021 -// -//Licensed under the Apache License, Version 2.0 (the "License"); -//you may not use this file except in compliance with the License. -//You may obtain a copy of the License at -// -//http://www.apache.org/licenses/LICENSE-2.0 -// -//Unless required by applicable law or agreed to in writing, software -//distributed under the License is distributed on an "AS IS" BASIS, -//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -//See the License for the specific language governing permissions and -//limitations under the License. - +// If this file is updated, protoc needs to be installed and the following command needs to be executed again in this directory // protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative fetchanswer.proto // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 -// protoc v3.17.3 +// protoc-gen-go v1.28.1-devel +// protoc v3.19.1 // source: fetchanswer.proto package qagrpc @@ -64,6 +50,7 @@ type Problem struct { Hints []string `protobuf:"bytes,4,rep,name=hints,proto3" json:"hints,omitempty"` Options []string `protobuf:"bytes,5,rep,name=options,proto3" json:"options,omitempty"` Default []string `protobuf:"bytes,6,rep,name=default,proto3" json:"default,omitempty"` + Pattern string `protobuf:"bytes,7,opt,name=pattern,proto3" json:"pattern,omitempty"` } func (x *Problem) Reset() { @@ -140,6 +127,13 @@ func (x *Problem) GetDefault() []string { return nil } +func (x *Problem) GetPattern() string { + if x != nil { + return x.Pattern + } + return "" +} + type Answer struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -191,7 +185,7 @@ var File_fetchanswer_proto protoreflect.FileDescriptor var file_fetchanswer_proto_rawDesc = []byte{ 0x0a, 0x11, 0x66, 0x65, 0x74, 0x63, 0x68, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x71, 0x61, 0x67, 0x72, 0x70, 0x63, 0x22, 0x99, 0x01, 0x0a, 0x07, + 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x71, 0x61, 0x67, 0x72, 0x70, 0x63, 0x22, 0xb3, 0x01, 0x0a, 0x07, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, @@ -201,17 +195,18 @@ var file_fetchanswer_proto_rawDesc = []byte{ 0x6e, 0x74, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, - 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x22, 0x20, 0x0a, 0x06, 0x41, 0x6e, 0x73, 0x77, 0x65, - 0x72, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x06, 0x61, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x32, 0x3c, 0x0a, 0x08, 0x51, 0x41, 0x45, - 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x12, 0x30, 0x0a, 0x0b, 0x46, 0x65, 0x74, 0x63, 0x68, 0x41, 0x6e, - 0x73, 0x77, 0x65, 0x72, 0x12, 0x0f, 0x2e, 0x71, 0x61, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, - 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x1a, 0x0e, 0x2e, 0x71, 0x61, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x41, - 0x6e, 0x73, 0x77, 0x65, 0x72, 0x22, 0x00, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x6f, 0x6e, 0x76, 0x65, 0x79, 0x6f, 0x72, 0x2f, 0x6d, - 0x6f, 0x76, 0x65, 0x32, 0x6b, 0x75, 0x62, 0x65, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x71, - 0x61, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2f, 0x71, 0x61, 0x67, 0x72, 0x70, 0x63, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, + 0x72, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, + 0x6e, 0x22, 0x20, 0x0a, 0x06, 0x41, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x61, + 0x6e, 0x73, 0x77, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6e, 0x73, + 0x77, 0x65, 0x72, 0x32, 0x3c, 0x0a, 0x08, 0x51, 0x41, 0x45, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x12, + 0x30, 0x0a, 0x0b, 0x46, 0x65, 0x74, 0x63, 0x68, 0x41, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x12, 0x0f, + 0x2e, 0x71, 0x61, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x1a, + 0x0e, 0x2e, 0x71, 0x61, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x6e, 0x73, 0x77, 0x65, 0x72, 0x22, + 0x00, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x6b, 0x6f, 0x6e, 0x76, 0x65, 0x79, 0x6f, 0x72, 0x2f, 0x6d, 0x6f, 0x76, 0x65, 0x32, 0x6b, 0x75, + 0x62, 0x65, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x71, 0x61, 0x65, 0x6e, 0x67, 0x69, 0x6e, + 0x65, 0x2f, 0x71, 0x61, 0x67, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/types/qaengine/qagrpc/fetchanswer.proto b/types/qaengine/qagrpc/fetchanswer.proto index 2bc721d66..3bdbff1c6 100644 --- a/types/qaengine/qagrpc/fetchanswer.proto +++ b/types/qaengine/qagrpc/fetchanswer.proto @@ -34,6 +34,7 @@ message Problem { repeated string hints = 4; repeated string options = 5; repeated string default = 6; + string pattern = 7; } message Answer { diff --git a/types/qaengine/qagrpc/fetchanswer_grpc.pb.go b/types/qaengine/qagrpc/fetchanswer_grpc.pb.go index 420f080c4..87390f4ae 100644 --- a/types/qaengine/qagrpc/fetchanswer_grpc.pb.go +++ b/types/qaengine/qagrpc/fetchanswer_grpc.pb.go @@ -1,5 +1,5 @@ /* - * Copyright IBM Corporation 2020, 2021 + * Copyright IBM Corporation 2020, 2021, 2022 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,10 @@ */ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.19.1 +// source: fetchanswer.proto package qagrpc