From b2e3e6f04185dca9545651e270080e04cf2dab35 Mon Sep 17 00:00:00 2001 From: Kyle Hodgetts Date: Wed, 18 Aug 2021 11:18:52 +0300 Subject: [PATCH 1/4] create internal wizard prompt package --- wizard/prompt/prompt.go | 124 +++++++++++++++++++++++++++++++++++ wizard/wizard.go | 142 +++++----------------------------------- 2 files changed, 142 insertions(+), 124 deletions(-) create mode 100644 wizard/prompt/prompt.go diff --git a/wizard/prompt/prompt.go b/wizard/prompt/prompt.go new file mode 100644 index 0000000..8ea910c --- /dev/null +++ b/wizard/prompt/prompt.go @@ -0,0 +1,124 @@ +package prompt + +import ( + "errors" + "os" + "strings" + + "github.com/manifoldco/promptui" +) + +func SelectOneOf(label string, variants []string, withAdd bool) string { + if len(variants) == 0 { + // it's better to show a prompt + return InputNonEmpty(label, "") + } + + if withAdd { + p := promptui.SelectWithAdd{ + Label: label, + Stdout: os.Stderr, + Items: variants, + } + + _, res, _ := p.Run() + return res + } + + p := promptui.Select{ + Label: label, + Stdout: os.Stderr, + Items: variants, + } + + _, res, _ := p.Run() + return res +} + +func Input(label, defaultString string) string { + p := promptui.Prompt{ + Label: label, + Stdout: os.Stderr, + Validate: func(s string) error { + return nil + }, + Default: defaultString, + } + + res, _ := p.Run() + + return res +} + +func InputNonEmpty(label, defaultString string) string { + p := promptui.Prompt{ + Label: label, + Stdout: os.Stderr, + Validate: func(s string) error { + if strings.TrimSpace(s) == "" { + return errors.New("should not be empty") + } + + return nil + }, + Default: defaultString, + } + + res, _ := p.Run() + + return res +} + +func FilePath(label, defaultPath string, shouldExist bool) string { + p := promptui.Prompt{ + Label: label, + Stdout: os.Stderr, + Default: defaultPath, + Validate: func(fp string) error { + if strings.TrimSpace(fp) == "" { + return errors.New("should not be empty") + } + + if !shouldExist { + return nil + } + + if fileExists(fp) { + return nil + } + + return errors.New("should be an existing file") + }, + } + + res, _ := p.Run() + + return res +} + +func Confirm(question string) bool { + p := promptui.Prompt{ + Label: question, + Stdout: os.Stderr, + IsConfirm: true, + } + + _, err := p.Run() + if err != nil { + if errors.Is(err, promptui.ErrAbort) { + return false + } + } + + return true +} + +func fileExists(path string) bool { + // check if file exists + f, err := os.Stat(path) + if err == nil && !f.IsDir() { + return true + } + + return false +} diff --git a/wizard/wizard.go b/wizard/wizard.go index cf434fa..c4ddd33 100644 --- a/wizard/wizard.go +++ b/wizard/wizard.go @@ -1,7 +1,6 @@ package wizard import ( - "errors" "fmt" "log" "os" @@ -9,7 +8,6 @@ import ( "strings" "github.com/getkin/kin-openapi/openapi3" - "github.com/manifoldco/promptui" "k8s.io/client-go/util/homedir" "github.com/kubeshop/kusk/cluster" @@ -17,6 +15,7 @@ import ( "github.com/kubeshop/kusk/generators/linkerd" "github.com/kubeshop/kusk/generators/nginx_ingress" "github.com/kubeshop/kusk/options" + "github.com/kubeshop/kusk/wizard/prompt" ) func Start(apiSpecPath string, apiSpec *openapi3.T) { @@ -26,7 +25,7 @@ func Start(apiSpecPath string, apiSpec *openapi3.T) { if fileExists(kubeConfigPath) { fmt.Printf("⎈ kubeconfig detected in %s\n", kubeConfigPath) - canConnectToCluster = confirm( + canConnectToCluster = prompt.Confirm( "Can Kusk connect to your current cluster to check for supported services and provide suggestions?", ) } @@ -46,8 +45,8 @@ func Start(apiSpecPath string, apiSpec *openapi3.T) { fmt.Fprintln(os.Stderr, "✔ Done!") - if confirm("Do you want to save mappings to a file (otherwise output to stdout)") { - saveToPath := promptFilePath("Save to", "generated.yaml", false) + if prompt.Confirm("Do you want to save mappings to a file (otherwise output to stdout)") { + saveToPath := prompt.FilePath("Save to", "generated.yaml", false) err := os.WriteFile(saveToPath, []byte(mappings), 0666) if err != nil { @@ -108,7 +107,7 @@ func flowWithCluster(apiSpecPath string, apiSpec *openapi3.T, kubeConfigPath str return "", fmt.Errorf("failed to list namespaces: %w", err) } - targetServiceNamespace = selectOneOf("Choose namespace with your service", targetServiceNamespaceSuggestions, true) + targetServiceNamespace = prompt.SelectOneOf("Choose namespace with your service", targetServiceNamespaceSuggestions, true) var targetServiceSuggestions []string var targetService string @@ -118,9 +117,9 @@ func flowWithCluster(apiSpecPath string, apiSpec *openapi3.T, kubeConfigPath str return "", fmt.Errorf("failed to list namespaces: %w", err) } - targetService = selectOneOf("Choose your service", targetServiceSuggestions, true) + targetService = prompt.SelectOneOf("Choose your service", targetServiceSuggestions, true) - service := selectOneOf("Choose a service you want Kusk generate manifests for", servicesToSuggest, false) + service := prompt.SelectOneOf("Choose a service you want Kusk generate manifests for", servicesToSuggest, false) switch service { case "ambassador": @@ -135,10 +134,10 @@ func flowWithCluster(apiSpecPath string, apiSpec *openapi3.T, kubeConfigPath str } func flowWithoutCluster(apiSpecPath string, apiSpec *openapi3.T) (string, error) { - targetServiceNamespace := promptStringNonEmpty("Enter namespace with your service", "default") - targetService := promptStringNonEmpty("Enter your service name", "") + targetServiceNamespace := prompt.InputNonEmpty("Enter namespace with your service", "default") + targetService := prompt.InputNonEmpty("Enter your service name", "") - service := selectOneOf( + service := prompt.SelectOneOf( "Choose a service you want Kusk generate manifests for", []string{ "ambassador", @@ -167,13 +166,13 @@ func flowAmbassador(apiSpecPath string, apiSpec *openapi3.T, targetNamespace, ta basePathSuggestions = append(basePathSuggestions, server.URL) } - basePath := selectOneOf("Base path prefix", basePathSuggestions, true) - trimPrefix := promptStringNonEmpty("Prefix to trim from the URL (rewrite)", basePath) + basePath := prompt.SelectOneOf("Base path prefix", basePathSuggestions, true) + trimPrefix := prompt.InputNonEmpty("Prefix to trim from the URL (rewrite)", basePath) separateMappings := false if basePath != "" { - separateMappings = confirm("Generate mapping for each endpoint separately?") + separateMappings = prompt.Confirm("Generate mapping for each endpoint separately?") } opts := &options.Options{ @@ -215,7 +214,7 @@ func flowAmbassador(apiSpecPath string, apiSpec *openapi3.T, targetNamespace, ta } func flowLinkerd(apiSpecPath string, apiSpec *openapi3.T, targetNamespace, targetService string) (string, error) { - clusterDomain := promptStringNonEmpty("Cluster domain", "cluster.local") + clusterDomain := prompt.InputNonEmpty("Cluster domain", "cluster.local") opts := &options.Options{ Namespace: targetNamespace, @@ -242,18 +241,18 @@ func flowLinkerd(apiSpecPath string, apiSpec *openapi3.T, targetNamespace, targe return ld.Generate(opts, apiSpec) } -func flowNginxIngress(apiSpecPath string, apiSpec *openapi3.T, targetNamespace, targetService string) (string, error){ +func flowNginxIngress(apiSpecPath string, apiSpec *openapi3.T, targetNamespace, targetService string) (string, error) { var basePathSuggestions []string for _, server := range apiSpec.Servers { basePathSuggestions = append(basePathSuggestions, server.URL) } - basePath := selectOneOf("Base path prefix", basePathSuggestions, true) - trimPrefix := promptString("Prefix to trim from the URL (rewrite)", "") + basePath := prompt.SelectOneOf("Base path prefix", basePathSuggestions, true) + trimPrefix := prompt.Input("Prefix to trim from the URL (rewrite)", "") separateMappings := false if basePath != "" { - separateMappings = confirm("Generate ingress resource for each endpoint separately?") + separateMappings = prompt.Confirm("Generate ingress resource for each endpoint separately?") } opts := &options.Options{ @@ -296,67 +295,6 @@ func flowNginxIngress(apiSpecPath string, apiSpec *openapi3.T, targetNamespace, return ingresses, nil } -func selectOneOf(label string, variants []string, withAdd bool) string { - if len(variants) == 0 { - // it's better to show a prompt - return promptStringNonEmpty(label, "") - } - - if withAdd { - p := promptui.SelectWithAdd{ - Label: label, - Stdout: os.Stderr, - Items: variants, - } - - _, res, _ := p.Run() - return res - } - - p := promptui.Select{ - Label: label, - Stdout: os.Stderr, - Items: variants, - } - - _, res, _ := p.Run() - return res -} - -func promptString(label, defaultString string) string { - p := promptui.Prompt{ - Label: label, - Stdout: os.Stderr, - Validate: func(s string) error { - return nil - }, - Default: defaultString, - } - - res, _ := p.Run() - - return res -} - -func promptStringNonEmpty(label, defaultString string) string { - p := promptui.Prompt{ - Label: label, - Stdout: os.Stderr, - Validate: func(s string) error { - if strings.TrimSpace(s) == "" { - return errors.New("should not be empty") - } - - return nil - }, - Default: defaultString, - } - - res, _ := p.Run() - - return res -} - func fileExists(path string) bool { // check if file exists f, err := os.Stat(path) @@ -366,47 +304,3 @@ func fileExists(path string) bool { return false } - -func promptFilePath(label, defaultPath string, shouldExist bool) string { - p := promptui.Prompt{ - Label: label, - Stdout: os.Stderr, - Default: defaultPath, - Validate: func(fp string) error { - if strings.TrimSpace(fp) == "" { - return errors.New("should not be empty") - } - - if !shouldExist { - return nil - } - - if fileExists(fp) { - return nil - } - - return errors.New("should be an existing file") - }, - } - - res, _ := p.Run() - - return res -} - -func confirm(question string) bool { - p := promptui.Prompt{ - Label: question, - Stdout: os.Stderr, - IsConfirm: true, - } - - _, err := p.Run() - if err != nil { - if errors.Is(err, promptui.ErrAbort) { - return false - } - } - - return true -} From f218bd1ca1ec244c08479b65229f8d2c8e0aafa9 Mon Sep 17 00:00:00 2001 From: Kyle Hodgetts Date: Wed, 18 Aug 2021 11:20:03 +0300 Subject: [PATCH 2/4] refactor fileExists function by removing if statement and replacing it with equivalent return statement --- wizard/prompt/prompt.go | 6 +----- wizard/wizard.go | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/wizard/prompt/prompt.go b/wizard/prompt/prompt.go index 8ea910c..8c8b186 100644 --- a/wizard/prompt/prompt.go +++ b/wizard/prompt/prompt.go @@ -116,9 +116,5 @@ func Confirm(question string) bool { func fileExists(path string) bool { // check if file exists f, err := os.Stat(path) - if err == nil && !f.IsDir() { - return true - } - - return false + return err == nil && !f.IsDir() } diff --git a/wizard/wizard.go b/wizard/wizard.go index c4ddd33..20d951f 100644 --- a/wizard/wizard.go +++ b/wizard/wizard.go @@ -298,9 +298,5 @@ func flowNginxIngress(apiSpecPath string, apiSpec *openapi3.T, targetNamespace, func fileExists(path string) bool { // check if file exists f, err := os.Stat(path) - if err == nil && !f.IsDir() { - return true - } - - return false + return err == nil && !f.IsDir() } From 1bebc9aadc681eb77210635bd2d968123c049d46 Mon Sep 17 00:00:00 2001 From: Kyle Hodgetts Date: Wed, 18 Aug 2021 13:37:34 +0300 Subject: [PATCH 3/4] refactor each generator into own flow for wizard --- wizard/flow/ambassador.go | 66 +++++++++++++ wizard/flow/flow.go | 55 +++++++++++ wizard/flow/linkerd.go | 46 +++++++++ wizard/flow/nginx_ingress.go | 68 +++++++++++++ wizard/wizard.go | 186 +++++------------------------------ 5 files changed, 262 insertions(+), 159 deletions(-) create mode 100644 wizard/flow/ambassador.go create mode 100644 wizard/flow/flow.go create mode 100644 wizard/flow/linkerd.go create mode 100644 wizard/flow/nginx_ingress.go diff --git a/wizard/flow/ambassador.go b/wizard/flow/ambassador.go new file mode 100644 index 0000000..8731236 --- /dev/null +++ b/wizard/flow/ambassador.go @@ -0,0 +1,66 @@ +package flow + +import ( + "fmt" + + "github.com/kubeshop/kusk/generators/ambassador" + "github.com/kubeshop/kusk/options" + "github.com/kubeshop/kusk/wizard/prompt" +) + +type ambassadorFlow struct { + baseFlow +} + +func (a ambassadorFlow) Start() (Response, error) { + var basePathSuggestions []string + for _, server := range a.apiSpec.Servers { + basePathSuggestions = append(basePathSuggestions, server.URL) + } + + basePath := prompt.SelectOneOf("Base path prefix", basePathSuggestions, true) + trimPrefix := prompt.InputNonEmpty("Prefix to trim from the URL (rewrite)", basePath) + + separateMappings := false + + if basePath != "" { + separateMappings = prompt.Confirm("Generate mapping for each endpoint separately?") + } + + opts := &options.Options{ + Namespace: a.targetNamespace, + Service: options.ServiceOptions{ + Namespace: a.targetNamespace, + Name: a.targetService, + }, + Path: options.PathOptions{ + Base: basePath, + TrimPrefix: trimPrefix, + Split: separateMappings, + }, + } + + cmd := fmt.Sprintf("kusk ambassador -i %s ", a.apiSpecPath) + cmd = cmd + fmt.Sprintf("--namespace=%s ", a.targetNamespace) + cmd = cmd + fmt.Sprintf("--service.namespace=%s ", a.targetNamespace) + cmd = cmd + fmt.Sprintf("--service.name=%s ", a.targetService) + cmd = cmd + fmt.Sprintf("--path.base=%s ", basePath) + if trimPrefix != "" { + cmd = cmd + fmt.Sprintf("--path.trim_prefix=%s ", trimPrefix) + } + if separateMappings { + cmd = cmd + fmt.Sprintf("--path.split ") + } + + var ag ambassador.Generator + + mappings, err := ag.Generate(opts, a.apiSpec) + if err != nil { + return Response{}, fmt.Errorf("Failed to generate mappings: %s\n", err) + } + + return Response{ + EquivalentCmd: cmd, + Manifests: mappings, + }, nil +} diff --git a/wizard/flow/flow.go b/wizard/flow/flow.go new file mode 100644 index 0000000..b607e81 --- /dev/null +++ b/wizard/flow/flow.go @@ -0,0 +1,55 @@ +package flow + +import ( + "fmt" + + "github.com/getkin/kin-openapi/openapi3" +) + +type Interface interface { + Start() (Response, error) +} + +type Response struct { + EquivalentCmd string + Manifests string +} + +// Flows "inherit" from this +type baseFlow struct { + apiSpecPath string + apiSpec *openapi3.T + targetNamespace string + targetService string +} + +type Args struct { + Service string + + ApiSpecPath string + ApiSpec *openapi3.T + TargetNamespace string + TargetService string +} + +// New returns a new flow based on the args.Service +// returns an error if the service isn't supported by a flow +func New(args *Args) (Interface, error) { + baseFlow := baseFlow{ + apiSpecPath: args.ApiSpecPath, + apiSpec: args.ApiSpec, + targetNamespace: args.TargetNamespace, + targetService: args.TargetService, + } + + switch args.Service { + case "ambassador": + return ambassadorFlow{baseFlow}, nil + case "linkerd": + return linkerdFlow{baseFlow}, nil + case "nginx-ingress": + return nginxIngressFlow{baseFlow}, nil + default: + return nil, fmt.Errorf("unsupported service: %s\n", args.Service) + } +} \ No newline at end of file diff --git a/wizard/flow/linkerd.go b/wizard/flow/linkerd.go new file mode 100644 index 0000000..74a711c --- /dev/null +++ b/wizard/flow/linkerd.go @@ -0,0 +1,46 @@ +package flow + +import ( + "fmt" + + "github.com/kubeshop/kusk/generators/linkerd" + "github.com/kubeshop/kusk/options" + "github.com/kubeshop/kusk/wizard/prompt" +) + +type linkerdFlow struct { + baseFlow +} + +func (l linkerdFlow) Start() (Response, error) { + clusterDomain := prompt.InputNonEmpty("Cluster domain", "cluster.local") + + opts := &options.Options{ + Namespace: l.targetNamespace, + Service: options.ServiceOptions{ + Namespace: l.targetNamespace, + Name: l.targetService, + }, + Cluster: options.ClusterOptions{ + ClusterDomain: clusterDomain, + }, + } + + cmd := fmt.Sprintf("kusk linkerd -i %s ", l.apiSpecPath) + cmd = cmd + fmt.Sprintf("--namespace=%s ", l.targetNamespace) + cmd = cmd + fmt.Sprintf("--service.namespace=%s ", l.targetNamespace) + cmd = cmd + fmt.Sprintf("--service.name=%s ", l.targetService) + cmd = cmd + fmt.Sprintf("--cluster.cluster_domain=%s ", clusterDomain) + + var ld linkerd.Generator + + serviceProfiles, err := ld.Generate(opts, l.apiSpec) + if err != nil { + return Response{}, fmt.Errorf("failed to generate linkerd service profiles: %s\n", err) + } + + return Response{ + EquivalentCmd: cmd, + Manifests: serviceProfiles, + }, nil +} \ No newline at end of file diff --git a/wizard/flow/nginx_ingress.go b/wizard/flow/nginx_ingress.go new file mode 100644 index 0000000..eee3f0f --- /dev/null +++ b/wizard/flow/nginx_ingress.go @@ -0,0 +1,68 @@ +package flow + +import ( + "fmt" + "strings" + + "github.com/kubeshop/kusk/generators/nginx_ingress" + "github.com/kubeshop/kusk/options" + "github.com/kubeshop/kusk/wizard/prompt" +) + +type nginxIngressFlow struct { + baseFlow +} + +func (n nginxIngressFlow) Start() (Response, error) { + var basePathSuggestions []string + for _, server := range n.apiSpec.Servers { + basePathSuggestions = append(basePathSuggestions, server.URL) + } + + basePath := prompt.SelectOneOf("Base path prefix", basePathSuggestions, true) + trimPrefix := prompt.Input("Prefix to trim from the URL (rewrite)", "") + + separateMappings := false + if basePath != "" { + separateMappings = prompt.Confirm("Generate ingress resource for each endpoint separately?") + } + + opts := &options.Options{ + Namespace: n.targetNamespace, + Service: options.ServiceOptions{ + Namespace: n.targetNamespace, + Name: n.targetService, + }, + Path: options.PathOptions{ + Base: basePath, + TrimPrefix: trimPrefix, + Split: separateMappings, + }, + } + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("kusk ambassador -i %s ", n.apiSpecPath)) + sb.WriteString(fmt.Sprintf("--namespace=%s ", n.targetNamespace)) + sb.WriteString(fmt.Sprintf("--service.namespace=%s ", n.targetNamespace)) + sb.WriteString(fmt.Sprintf("--service.name=%s ", n.targetService)) + sb.WriteString(fmt.Sprintf("--path.base=%s ", basePath)) + + if trimPrefix != "" { + sb.WriteString(fmt.Sprintf("--path.trim_prefix=%s ", trimPrefix)) + } + + if separateMappings { + sb.WriteString("--path.split ") + } + + var ingressGenerator nginx_ingress.Generator + ingresses, err := ingressGenerator.Generate(opts, n.apiSpec) + if err != nil { + return Response{}, fmt.Errorf("Failed to generate ingresses: %s\n", err) + } + + return Response{ + EquivalentCmd: sb.String(), + Manifests: ingresses, + }, nil +} \ No newline at end of file diff --git a/wizard/wizard.go b/wizard/wizard.go index 20d951f..4408b0f 100644 --- a/wizard/wizard.go +++ b/wizard/wizard.go @@ -5,16 +5,12 @@ import ( "log" "os" "path/filepath" - "strings" "github.com/getkin/kin-openapi/openapi3" "k8s.io/client-go/util/homedir" "github.com/kubeshop/kusk/cluster" - "github.com/kubeshop/kusk/generators/ambassador" - "github.com/kubeshop/kusk/generators/linkerd" - "github.com/kubeshop/kusk/generators/nginx_ingress" - "github.com/kubeshop/kusk/options" + "github.com/kubeshop/kusk/wizard/flow" "github.com/kubeshop/kusk/wizard/prompt" ) @@ -109,35 +105,32 @@ func flowWithCluster(apiSpecPath string, apiSpec *openapi3.T, kubeConfigPath str targetServiceNamespace = prompt.SelectOneOf("Choose namespace with your service", targetServiceNamespaceSuggestions, true) - var targetServiceSuggestions []string - var targetService string - - targetServiceSuggestions, err = client.ListServices(targetServiceNamespace) + targetServiceSuggestions, err := client.ListServices(targetServiceNamespace) if err != nil { return "", fmt.Errorf("failed to list namespaces: %w", err) } - targetService = prompt.SelectOneOf("Choose your service", targetServiceSuggestions, true) + args := &flow.Args{ + ApiSpecPath: apiSpecPath, + ApiSpec: apiSpec, + } - service := prompt.SelectOneOf("Choose a service you want Kusk generate manifests for", servicesToSuggest, false) + args.TargetService = prompt.SelectOneOf("Choose your service", targetServiceSuggestions, true) - switch service { - case "ambassador": - return flowAmbassador(apiSpecPath, apiSpec, targetServiceNamespace, targetService) - case "linkerd": - return flowLinkerd(apiSpecPath, apiSpec, targetServiceNamespace, targetService) - case "nginx-ingress": - return flowNginxIngress(apiSpecPath, apiSpec, targetServiceNamespace, targetService) - } + args.Service = prompt.SelectOneOf("Choose a service you want Kusk generate manifests for", servicesToSuggest, false) - return "", fmt.Errorf("unknown service") + return executeFlow(args) } func flowWithoutCluster(apiSpecPath string, apiSpec *openapi3.T) (string, error) { - targetServiceNamespace := prompt.InputNonEmpty("Enter namespace with your service", "default") - targetService := prompt.InputNonEmpty("Enter your service name", "") + args := &flow.Args{ + ApiSpecPath: apiSpecPath, + ApiSpec: apiSpec, + } + args.TargetNamespace = prompt.InputNonEmpty("Enter namespace with your service", "default") + args.TargetService = prompt.InputNonEmpty("Enter your service name", "") - service := prompt.SelectOneOf( + args.Service = prompt.SelectOneOf( "Choose a service you want Kusk generate manifests for", []string{ "ambassador", @@ -147,152 +140,27 @@ func flowWithoutCluster(apiSpecPath string, apiSpec *openapi3.T) (string, error) false, ) - switch service { - case "ambassador": - return flowAmbassador(apiSpecPath, apiSpec, targetServiceNamespace, targetService) - case "linkerd": - return flowLinkerd(apiSpecPath, apiSpec, targetServiceNamespace, targetService) - case "nginx-ingress": - return flowNginxIngress(apiSpecPath, apiSpec, targetServiceNamespace, targetService) - - } - - return "", fmt.Errorf("unknown service") + return executeFlow(args) } -func flowAmbassador(apiSpecPath string, apiSpec *openapi3.T, targetNamespace, targetService string) (string, error) { - var basePathSuggestions []string - for _, server := range apiSpec.Servers { - basePathSuggestions = append(basePathSuggestions, server.URL) - } - - basePath := prompt.SelectOneOf("Base path prefix", basePathSuggestions, true) - trimPrefix := prompt.InputNonEmpty("Prefix to trim from the URL (rewrite)", basePath) - - separateMappings := false - - if basePath != "" { - separateMappings = prompt.Confirm("Generate mapping for each endpoint separately?") - } - - opts := &options.Options{ - Namespace: targetNamespace, - Service: options.ServiceOptions{ - Namespace: targetNamespace, - Name: targetService, - }, - Path: options.PathOptions{ - Base: basePath, - TrimPrefix: trimPrefix, - Split: separateMappings, - }, - } - - cmd := fmt.Sprintf("kusk ambassador -i %s ", apiSpecPath) - cmd = cmd + fmt.Sprintf("--namespace=%s ", targetNamespace) - cmd = cmd + fmt.Sprintf("--service.namespace=%s ", targetNamespace) - cmd = cmd + fmt.Sprintf("--service.name=%s ", targetService) - cmd = cmd + fmt.Sprintf("--path.base=%s ", basePath) - if trimPrefix != "" { - cmd = cmd + fmt.Sprintf("--path.trim_prefix=%s ", trimPrefix) - } - if separateMappings { - cmd = cmd + fmt.Sprintf("--path.split ") - } - - fmt.Fprintln(os.Stderr, "Here is a CLI command you could use in your scripts (you can pipe it to kubectl):") - fmt.Fprintln(os.Stderr, cmd) - - var ag ambassador.Generator - - mappings, err := ag.Generate(opts, apiSpec) +func executeFlow(args *flow.Args) (string, error) { + f, err := flow.New(args) if err != nil { - log.Fatalf("Failed to generate mappings: %s\n", err) - } - - return mappings, nil -} - -func flowLinkerd(apiSpecPath string, apiSpec *openapi3.T, targetNamespace, targetService string) (string, error) { - clusterDomain := prompt.InputNonEmpty("Cluster domain", "cluster.local") - - opts := &options.Options{ - Namespace: targetNamespace, - Service: options.ServiceOptions{ - Namespace: targetNamespace, - Name: targetService, - }, - Cluster: options.ClusterOptions{ - ClusterDomain: clusterDomain, - }, - } - - cmd := fmt.Sprintf("kusk linkerd -i %s ", apiSpecPath) - cmd = cmd + fmt.Sprintf("--namespace=%s ", targetNamespace) - cmd = cmd + fmt.Sprintf("--service.namespace=%s ", targetNamespace) - cmd = cmd + fmt.Sprintf("--service.name=%s ", targetService) - cmd = cmd + fmt.Sprintf("--cluster.cluster_domain=%s ", clusterDomain) - - fmt.Fprintln(os.Stderr, "Here is a CLI command you could use in your scripts (you can pipe it to kubectl):") - fmt.Fprintln(os.Stderr, cmd) - - var ld linkerd.Generator - - return ld.Generate(opts, apiSpec) -} - -func flowNginxIngress(apiSpecPath string, apiSpec *openapi3.T, targetNamespace, targetService string) (string, error) { - var basePathSuggestions []string - for _, server := range apiSpec.Servers { - basePathSuggestions = append(basePathSuggestions, server.URL) + return "", fmt.Errorf("failed to create new flow: %s\n", err) } - basePath := prompt.SelectOneOf("Base path prefix", basePathSuggestions, true) - trimPrefix := prompt.Input("Prefix to trim from the URL (rewrite)", "") - - separateMappings := false - if basePath != "" { - separateMappings = prompt.Confirm("Generate ingress resource for each endpoint separately?") - } - - opts := &options.Options{ - Namespace: targetNamespace, - Service: options.ServiceOptions{ - Namespace: targetNamespace, - Name: targetService, - }, - Path: options.PathOptions{ - Base: basePath, - TrimPrefix: trimPrefix, - Split: separateMappings, - }, - } - - var sb strings.Builder - sb.WriteString(fmt.Sprintf("kusk ambassador -i %s ", apiSpecPath)) - sb.WriteString(fmt.Sprintf("--namespace=%s ", targetNamespace)) - sb.WriteString(fmt.Sprintf("--service.namespace=%s ", targetNamespace)) - sb.WriteString(fmt.Sprintf("--service.name=%s ", targetService)) - sb.WriteString(fmt.Sprintf("--path.base=%s ", basePath)) - - if trimPrefix != "" { - sb.WriteString(fmt.Sprintf("--path.trim_prefix=%s ", trimPrefix)) - } - - if separateMappings { - sb.WriteString("--path.split ") + response, err := f.Start() + if err != nil { + return "", fmt.Errorf("failed to execute flow: %s\n", err) } - fmt.Fprintln(os.Stderr, "Here is a CLI command you could use in your scripts (you can pipe it to kubectl):") - fmt.Fprintln(os.Stderr, sb.String()) - var ingressGenerator nginx_ingress.Generator - ingresses, err := ingressGenerator.Generate(opts, apiSpec) - if err != nil { - log.Fatalf("Failed to generate ingresses: %s\n", err) + if response.EquivalentCmd != "" { + fmt.Fprintln(os.Stderr, "Here is a CLI command you could use in your scripts (you can pipe it to kubectl):") + fmt.Fprintln(os.Stderr, response.EquivalentCmd) } - return ingresses, nil + return response.Manifests, nil } func fileExists(path string) bool { From 30bb0a477770aff72e41f54bd515f94c78bc4671 Mon Sep 17 00:00:00 2001 From: Kyle Hodgetts Date: Wed, 18 Aug 2021 13:58:40 +0300 Subject: [PATCH 4/4] Introduce prompter interface and implementation. Will be useful when it comes to writing tests as we can implement a test prompter that returns set values. The default implementation of the prompter requires an interactice stdin session --- cmd/wizard.go | 3 ++- wizard/flow/ambassador.go | 9 ++++----- wizard/flow/flow.go | 23 +++++++++++++++-------- wizard/flow/linkerd.go | 5 ++--- wizard/flow/nginx_ingress.go | 9 ++++----- wizard/prompt/prompt.go | 26 ++++++++++++++++++++------ wizard/wizard.go | 21 +++++++++++---------- 7 files changed, 58 insertions(+), 38 deletions(-) diff --git a/cmd/wizard.go b/cmd/wizard.go index bd603ae..1726a9b 100644 --- a/cmd/wizard.go +++ b/cmd/wizard.go @@ -9,6 +9,7 @@ import ( "github.com/kubeshop/kusk/spec" "github.com/kubeshop/kusk/wizard" + "github.com/kubeshop/kusk/wizard/prompt" ) func init() { @@ -28,7 +29,7 @@ func init() { log.Fatal(err) } - wizard.Start(apiSpecPath, apiSpec) + wizard.Start(apiSpecPath, apiSpec, prompt.New()) }, } diff --git a/wizard/flow/ambassador.go b/wizard/flow/ambassador.go index 8731236..35ea94a 100644 --- a/wizard/flow/ambassador.go +++ b/wizard/flow/ambassador.go @@ -5,7 +5,6 @@ import ( "github.com/kubeshop/kusk/generators/ambassador" "github.com/kubeshop/kusk/options" - "github.com/kubeshop/kusk/wizard/prompt" ) type ambassadorFlow struct { @@ -18,13 +17,13 @@ func (a ambassadorFlow) Start() (Response, error) { basePathSuggestions = append(basePathSuggestions, server.URL) } - basePath := prompt.SelectOneOf("Base path prefix", basePathSuggestions, true) - trimPrefix := prompt.InputNonEmpty("Prefix to trim from the URL (rewrite)", basePath) + basePath := a.prompt.SelectOneOf("Base path prefix", basePathSuggestions, true) + trimPrefix := a.prompt.InputNonEmpty("Prefix to trim from the URL (rewrite)", basePath) separateMappings := false if basePath != "" { - separateMappings = prompt.Confirm("Generate mapping for each endpoint separately?") + separateMappings = a.prompt.Confirm("Generate mapping for each endpoint separately?") } opts := &options.Options{ @@ -61,6 +60,6 @@ func (a ambassadorFlow) Start() (Response, error) { return Response{ EquivalentCmd: cmd, - Manifests: mappings, + Manifests: mappings, }, nil } diff --git a/wizard/flow/flow.go b/wizard/flow/flow.go index b607e81..32b08dd 100644 --- a/wizard/flow/flow.go +++ b/wizard/flow/flow.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/getkin/kin-openapi/openapi3" + + "github.com/kubeshop/kusk/wizard/prompt" ) type Interface interface { @@ -12,24 +14,28 @@ type Interface interface { type Response struct { EquivalentCmd string - Manifests string + Manifests string } // Flows "inherit" from this type baseFlow struct { - apiSpecPath string - apiSpec *openapi3.T + apiSpecPath string + apiSpec *openapi3.T targetNamespace string - targetService string + targetService string + + prompt prompt.Prompter } type Args struct { Service string - ApiSpecPath string - ApiSpec *openapi3.T + ApiSpecPath string + ApiSpec *openapi3.T TargetNamespace string - TargetService string + TargetService string + + Prompt prompt.Prompter } // New returns a new flow based on the args.Service @@ -40,6 +46,7 @@ func New(args *Args) (Interface, error) { apiSpec: args.ApiSpec, targetNamespace: args.TargetNamespace, targetService: args.TargetService, + prompt: args.Prompt, } switch args.Service { @@ -52,4 +59,4 @@ func New(args *Args) (Interface, error) { default: return nil, fmt.Errorf("unsupported service: %s\n", args.Service) } -} \ No newline at end of file +} diff --git a/wizard/flow/linkerd.go b/wizard/flow/linkerd.go index 74a711c..7fedaf7 100644 --- a/wizard/flow/linkerd.go +++ b/wizard/flow/linkerd.go @@ -5,7 +5,6 @@ import ( "github.com/kubeshop/kusk/generators/linkerd" "github.com/kubeshop/kusk/options" - "github.com/kubeshop/kusk/wizard/prompt" ) type linkerdFlow struct { @@ -13,7 +12,7 @@ type linkerdFlow struct { } func (l linkerdFlow) Start() (Response, error) { - clusterDomain := prompt.InputNonEmpty("Cluster domain", "cluster.local") + clusterDomain := l.prompt.InputNonEmpty("Cluster domain", "cluster.local") opts := &options.Options{ Namespace: l.targetNamespace, @@ -43,4 +42,4 @@ func (l linkerdFlow) Start() (Response, error) { EquivalentCmd: cmd, Manifests: serviceProfiles, }, nil -} \ No newline at end of file +} diff --git a/wizard/flow/nginx_ingress.go b/wizard/flow/nginx_ingress.go index eee3f0f..08c4e97 100644 --- a/wizard/flow/nginx_ingress.go +++ b/wizard/flow/nginx_ingress.go @@ -6,7 +6,6 @@ import ( "github.com/kubeshop/kusk/generators/nginx_ingress" "github.com/kubeshop/kusk/options" - "github.com/kubeshop/kusk/wizard/prompt" ) type nginxIngressFlow struct { @@ -19,12 +18,12 @@ func (n nginxIngressFlow) Start() (Response, error) { basePathSuggestions = append(basePathSuggestions, server.URL) } - basePath := prompt.SelectOneOf("Base path prefix", basePathSuggestions, true) - trimPrefix := prompt.Input("Prefix to trim from the URL (rewrite)", "") + basePath := n.prompt.SelectOneOf("Base path prefix", basePathSuggestions, true) + trimPrefix := n.prompt.Input("Prefix to trim from the URL (rewrite)", "") separateMappings := false if basePath != "" { - separateMappings = prompt.Confirm("Generate ingress resource for each endpoint separately?") + separateMappings = n.prompt.Confirm("Generate ingress resource for each endpoint separately?") } opts := &options.Options{ @@ -65,4 +64,4 @@ func (n nginxIngressFlow) Start() (Response, error) { EquivalentCmd: sb.String(), Manifests: ingresses, }, nil -} \ No newline at end of file +} diff --git a/wizard/prompt/prompt.go b/wizard/prompt/prompt.go index 8c8b186..32b2528 100644 --- a/wizard/prompt/prompt.go +++ b/wizard/prompt/prompt.go @@ -8,10 +8,24 @@ import ( "github.com/manifoldco/promptui" ) -func SelectOneOf(label string, variants []string, withAdd bool) string { +type Prompter interface { + SelectOneOf(label string, variants []string, withAdd bool) string + Input(label, defaultString string) string + InputNonEmpty(label, defaultString string) string + FilePath(label, defaultPath string, shouldExist bool) string + Confirm(question string) bool +} + +type prompter struct{} + +func New() Prompter { + return prompter{} +} + +func (pr prompter) SelectOneOf(label string, variants []string, withAdd bool) string { if len(variants) == 0 { // it's better to show a prompt - return InputNonEmpty(label, "") + return pr.InputNonEmpty(label, "") } if withAdd { @@ -35,7 +49,7 @@ func SelectOneOf(label string, variants []string, withAdd bool) string { return res } -func Input(label, defaultString string) string { +func (_ prompter) Input(label, defaultString string) string { p := promptui.Prompt{ Label: label, Stdout: os.Stderr, @@ -50,7 +64,7 @@ func Input(label, defaultString string) string { return res } -func InputNonEmpty(label, defaultString string) string { +func (_ prompter) InputNonEmpty(label, defaultString string) string { p := promptui.Prompt{ Label: label, Stdout: os.Stderr, @@ -69,7 +83,7 @@ func InputNonEmpty(label, defaultString string) string { return res } -func FilePath(label, defaultPath string, shouldExist bool) string { +func (_ prompter) FilePath(label, defaultPath string, shouldExist bool) string { p := promptui.Prompt{ Label: label, Stdout: os.Stderr, @@ -96,7 +110,7 @@ func FilePath(label, defaultPath string, shouldExist bool) string { return res } -func Confirm(question string) bool { +func (_ prompter) Confirm(question string) bool { p := promptui.Prompt{ Label: question, Stdout: os.Stderr, diff --git a/wizard/wizard.go b/wizard/wizard.go index 4408b0f..451a70b 100644 --- a/wizard/wizard.go +++ b/wizard/wizard.go @@ -14,7 +14,7 @@ import ( "github.com/kubeshop/kusk/wizard/prompt" ) -func Start(apiSpecPath string, apiSpec *openapi3.T) { +func Start(apiSpecPath string, apiSpec *openapi3.T, prompt prompt.Prompter) { canConnectToCluster := false kubeConfigPath := filepath.Join(homedir.HomeDir(), ".kube", "config") @@ -30,9 +30,9 @@ func Start(apiSpecPath string, apiSpec *openapi3.T) { var err error if canConnectToCluster { - mappings, err = flowWithCluster(apiSpecPath, apiSpec, kubeConfigPath) + mappings, err = flowWithCluster(apiSpecPath, apiSpec, kubeConfigPath, prompt) } else { - mappings, err = flowWithoutCluster(apiSpecPath, apiSpec) + mappings, err = flowWithoutCluster(apiSpecPath, apiSpec, prompt) } if err != nil { @@ -55,7 +55,7 @@ func Start(apiSpecPath string, apiSpec *openapi3.T) { fmt.Println(mappings) } -func flowWithCluster(apiSpecPath string, apiSpec *openapi3.T, kubeConfigPath string) (string, error) { +func flowWithCluster(apiSpecPath string, apiSpec *openapi3.T, kubeConfigPath string, prompt prompt.Prompter) (string, error) { var servicesToSuggest []string client, err := cluster.NewClient(kubeConfigPath) @@ -111,8 +111,9 @@ func flowWithCluster(apiSpecPath string, apiSpec *openapi3.T, kubeConfigPath str } args := &flow.Args{ - ApiSpecPath: apiSpecPath, - ApiSpec: apiSpec, + ApiSpecPath: apiSpecPath, + ApiSpec: apiSpec, + Prompt: prompt, } args.TargetService = prompt.SelectOneOf("Choose your service", targetServiceSuggestions, true) @@ -122,10 +123,11 @@ func flowWithCluster(apiSpecPath string, apiSpec *openapi3.T, kubeConfigPath str return executeFlow(args) } -func flowWithoutCluster(apiSpecPath string, apiSpec *openapi3.T) (string, error) { +func flowWithoutCluster(apiSpecPath string, apiSpec *openapi3.T, prompt prompt.Prompter) (string, error) { args := &flow.Args{ - ApiSpecPath: apiSpecPath, - ApiSpec: apiSpec, + ApiSpecPath: apiSpecPath, + ApiSpec: apiSpec, + Prompt: prompt, } args.TargetNamespace = prompt.InputNonEmpty("Enter namespace with your service", "default") args.TargetService = prompt.InputNonEmpty("Enter your service name", "") @@ -154,7 +156,6 @@ func executeFlow(args *flow.Args) (string, error) { return "", fmt.Errorf("failed to execute flow: %s\n", err) } - if response.EquivalentCmd != "" { fmt.Fprintln(os.Stderr, "Here is a CLI command you could use in your scripts (you can pipe it to kubectl):") fmt.Fprintln(os.Stderr, response.EquivalentCmd)