Skip to content

Commit

Permalink
Split Wizard up into testable components (#122)
Browse files Browse the repository at this point in the history
* 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
Show file tree
Hide file tree
Showing 7 changed files with 414 additions and 281 deletions.
3 changes: 2 additions & 1 deletion cmd/wizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/kubeshop/kusk/spec"
"github.com/kubeshop/kusk/wizard"
"github.com/kubeshop/kusk/wizard/prompt"
)

func init() {
Expand All @@ -28,7 +29,7 @@ func init() {
log.Fatal(err)
}

wizard.Start(apiSpecPath, apiSpec)
wizard.Start(apiSpecPath, apiSpec, prompt.New())
},
}

Expand Down
65 changes: 65 additions & 0 deletions wizard/flow/ambassador.go
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
}
62 changes: 62 additions & 0 deletions wizard/flow/flow.go
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)
}
}
45 changes: 45 additions & 0 deletions wizard/flow/linkerd.go
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
}
67 changes: 67 additions & 0 deletions wizard/flow/nginx_ingress.go
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
}
134 changes: 134 additions & 0 deletions wizard/prompt/prompt.go
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()
}
Loading

0 comments on commit 35f73eb

Please sign in to comment.