-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split Wizard up into testable components (#122)
* create internal wizard prompt package * refactor fileExists function by removing if statement and replacing it with equivalent return statement * refactor each generator into own flow for wizard * 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
- Loading branch information
Kyle Hodgetts
committed
Aug 18, 2021
1 parent
0e6ca1d
commit 35f73eb
Showing
7 changed files
with
414 additions
and
281 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package flow | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/kubeshop/kusk/generators/ambassador" | ||
"github.com/kubeshop/kusk/options" | ||
) | ||
|
||
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 := 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 = a.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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package flow | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/getkin/kin-openapi/openapi3" | ||
|
||
"github.com/kubeshop/kusk/wizard/prompt" | ||
) | ||
|
||
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 | ||
|
||
prompt prompt.Prompter | ||
} | ||
|
||
type Args struct { | ||
Service string | ||
|
||
ApiSpecPath string | ||
ApiSpec *openapi3.T | ||
TargetNamespace string | ||
TargetService string | ||
|
||
Prompt prompt.Prompter | ||
} | ||
|
||
// 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, | ||
prompt: args.Prompt, | ||
} | ||
|
||
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package flow | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/kubeshop/kusk/generators/linkerd" | ||
"github.com/kubeshop/kusk/options" | ||
) | ||
|
||
type linkerdFlow struct { | ||
baseFlow | ||
} | ||
|
||
func (l linkerdFlow) Start() (Response, error) { | ||
clusterDomain := l.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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package flow | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/kubeshop/kusk/generators/nginx_ingress" | ||
"github.com/kubeshop/kusk/options" | ||
) | ||
|
||
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 := n.prompt.SelectOneOf("Base path prefix", basePathSuggestions, true) | ||
trimPrefix := n.prompt.Input("Prefix to trim from the URL (rewrite)", "") | ||
|
||
separateMappings := false | ||
if basePath != "" { | ||
separateMappings = n.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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
package prompt | ||
|
||
import ( | ||
"errors" | ||
"os" | ||
"strings" | ||
|
||
"github.com/manifoldco/promptui" | ||
) | ||
|
||
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 pr.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 (_ prompter) 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 (_ prompter) 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 (_ prompter) 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 (_ prompter) 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) | ||
return err == nil && !f.IsDir() | ||
} |
Oops, something went wrong.