Skip to content
Open
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
53 changes: 2 additions & 51 deletions cmd/cli/completion.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
package cli

import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"

openai "github.com/sashabaranov/go-openai"
"github.com/sethvargo/go-retry"
log "github.com/sirupsen/logrus"
)

const maxRetries = 10
Expand All @@ -21,7 +12,7 @@ type oaiClients struct {
openAIClient openai.Client
}

func newOAIClients() (oaiClients, error) {
func newOAIClients() oaiClients {
var config openai.ClientConfig
config = openai.DefaultConfig(*openAIAPIKey)

Expand All @@ -45,45 +36,5 @@ func newOAIClients() (oaiClients, error) {
clients := oaiClients{
openAIClient: *openai.NewClientWithConfig(config),
}
return clients, nil
}

func gptCompletion(ctx context.Context, client oaiClients, prompts []string) (string, error) {
temp := float32(*temperature)
var prompt strings.Builder

// read from stdin
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
stdin, err := io.ReadAll(os.Stdin)
if err != nil {
return "", err
}
fmt.Fprintf(&prompt, "Depending on the input, either edit or append to the input YAML. Do not generate new YAML without including the input YAML either original or edited.\nUse the following YAML as the input: \n%s\n", string(stdin))
}

for _, p := range prompts {
fmt.Fprintf(&prompt, "%s", p)
}

var resp string
var err error
r := retry.WithMaxRetries(maxRetries, retry.NewExponential(1*time.Second))
if err := retry.Do(ctx, r, func(ctx context.Context) error {
resp, err = client.openaiGptChatCompletion(ctx, &prompt, temp)

requestErr := &openai.APIError{}
if errors.As(err, &requestErr) {
switch requestErr.HTTPStatusCode {
case http.StatusTooManyRequests, http.StatusRequestTimeout, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
log.Debugf("retrying due to status code %d: %s", requestErr.HTTPStatusCode, requestErr.Message)
return retry.RetryableError(err)
}
}
return nil
}); err != nil {
return "", err
}

return resp, nil
return clients
}
117 changes: 36 additions & 81 deletions cmd/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,20 @@ package cli

import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"strconv"

"github.com/charmbracelet/glamour"
"github.com/janeczku/go-spinner"
"github.com/manifoldco/promptui"
tea "github.com/charmbracelet/bubbletea"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"github.com/walles/env"
"k8s.io/cli-runtime/pkg/genericclioptions"
)

const (
apply = "Apply"
dontApply = "Don't Apply"
reprompt = "Reprompt"
)

var (
openaiAPIURLv1 = "https://api.openai.com/v1"
version = "dev"
Expand All @@ -47,6 +40,7 @@ func InitAndExecute() {
}

if err := RootCmd().Execute(); err != nil {
handleError(err)
os.Exit(1)
}
}
Expand Down Expand Up @@ -96,90 +90,51 @@ func run(args []string) error {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

oaiClients, err := newOAIClients()
if err != nil {
return err
var k8sContext string
currentContext, err := getCurrentContextName()
if err == nil {
log.Debugf("current-context: %s", currentContext)
k8sContext = currentContext
}

var action, completion string
for action != apply {
args = append(args, action)

s := spinner.NewSpinner("Processing...")
if !*debug && !*raw {
s.SetCharset([]string{"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"})
s.Start()
}

completion, err = gptCompletion(ctx, oaiClients, args)
if err != nil {
return err
}

s.Stop()

if *raw {
completion = trimTicks(completion)
fmt.Println(completion)
return nil
}
p := tea.NewProgram(newModel(args, k8sContext, !*requireConfirmation), tea.WithContext(ctx))
m, err := p.Run()
if err != nil {
return uiError{err, "Couldn't start Bubble Tea program."}
}

text := fmt.Sprintf("✨ Attempting to apply the following manifest:\n%s", completion)
r, err := glamour.NewTermRenderer(glamour.WithAutoStyle())
if err != nil {
return err
}
out, err := r.Render(text)
if err != nil {
return err
}
fmt.Print(out)
// remove unnessary backticks if they are in the output
completion = trimTicks(completion)
model, ok := m.(model)
if !ok {
return fmt.Errorf("unexpected model type %T", m)
} else if model.error != nil {
return *model.error
}

action, err = userActionPrompt()
if err != nil {
return err
}
// Create a manifest from the last completion
manifest := trimTicks(model.completion)

if action == dontApply {
return nil
}
if model.state == apply || model.state == autoApply {
return applyManifest(manifest)
}

return applyManifest(completion)
return nil
}

func userActionPrompt() (string, error) {
// if require confirmation is not set, immediately return apply
if !*requireConfirmation {
return apply, nil
}
func handleError(err error) {
format := "\n%s\n\n"

var result string
var err error
items := []string{apply, dontApply}
currentContext, err := getCurrentContextName()
label := fmt.Sprintf("Would you like to apply this? [%[1]s/%[2]s/%[3]s]", reprompt, apply, dontApply)
if err == nil {
label = fmt.Sprintf("(context: %[1]s) %[2]s", currentContext, label)
}
var args []interface{}
var merr uiError

prompt := promptui.SelectWithAdd{
Label: label,
Items: items,
AddLabel: reprompt,
}
_, result, err = prompt.Run()
if err != nil {
// workaround for bug in promptui when input is piped in from stdin
// however, this will not block for ui input
// for now, we will not apply the yaml, but user can pipe to input to kubectl
if err.Error() == "^D" {
return dontApply, nil
if errors.As(err, &merr) {
args = []interface{}{
stderrStyles().ErrPadding.Render(stderrStyles().ErrorHeader.String(), merr.reason),
}
} else {
args = []interface{}{
stderrStyles().ErrPadding.Render(stderrStyles().ErrorDetails.Render(err.Error())),
}
return dontApply, err
}

return result, nil
fmt.Fprintf(os.Stderr, format, args...)
}
36 changes: 36 additions & 0 deletions cmd/cli/term.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cli

import (
"os"
"sync"

"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/term"
"github.com/muesli/termenv"
)

var isInputTTY = sync.OnceValue(func() bool {
return term.IsTerminal(os.Stdin.Fd())
})

var stderrRenderer = sync.OnceValue(func() *lipgloss.Renderer {
return lipgloss.NewRenderer(os.Stderr, termenv.WithColorCache(true))
})

var stderrStyles = sync.OnceValue(func() styles {
return makeStyles(stderrRenderer())
})

type styles struct {
ErrorHeader,
ErrorDetails,
ErrPadding lipgloss.Style
}

func makeStyles(r *lipgloss.Renderer) (s styles) {
const horizontalEdgePadding = 2
s.ErrorHeader = r.NewStyle().Foreground(lipgloss.Color("#F1F1F1")).Background(lipgloss.Color("#FF5F87")).Bold(true).Padding(0, 1).SetString("ERROR")
s.ErrorDetails = r.NewStyle().Foreground(lipgloss.Color("#757575"))
s.ErrPadding = r.NewStyle().Padding(0, horizontalEdgePadding)
return s
}
Loading