Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Split Wizard up into testable components #122

Merged
merged 4 commits into from
Aug 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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