Skip to content
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
11 changes: 10 additions & 1 deletion otdfctl/cmd/migrate/namespaced_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,20 @@ func migrateNamespacedPolicy(cmd *cobra.Command, args []string) {
if err != nil {
cli.ExitWithError("could not read --commit flag", err)
}
interactive, err := cmd.InheritedFlags().GetBool("interactive")
if err != nil {
cli.ExitWithError("could not read --interactive flag", err)
}

h := otdfctl.NewHandler(c)
defer h.Close()

planner, err := namespacedpolicy.NewPlanner(&h, scopeCSV)
var plannerOpts []namespacedpolicy.Option
if interactive {
plannerOpts = append(plannerOpts, namespacedpolicy.WithInteractiveReviewer(namespacedpolicy.NewHuhInteractiveReviewer(&h, nil)))
}

planner, err := namespacedpolicy.NewPlanner(&h, scopeCSV, plannerOpts...)
if err != nil {
cli.ExitWithError("could not create namespaced-policy planner", err)
}
Expand Down
5 changes: 3 additions & 2 deletions otdfctl/migrations/namespacedpolicy/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var (
const (
migrationLabelMigratedFrom = "migrated_from"
migrationLabelRun = "migration_run"
unknownLabel = "<unknown>"
)

type ExecutorHandler interface {
Expand Down Expand Up @@ -133,13 +134,13 @@ func namespaceIdentifier(namespace *policy.Namespace) string {

func namespaceLabel(namespace *policy.Namespace) string {
if namespace == nil {
return "<unknown>"
return unknownLabel
}
if fqn := strings.TrimSpace(namespace.GetFqn()); fqn != "" {
return fqn
}
if id := strings.TrimSpace(namespace.GetId()); id != "" {
return id
}
return "<unknown>"
return unknownLabel
}
124 changes: 124 additions & 0 deletions otdfctl/migrations/namespacedpolicy/interactive_prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package namespacedpolicy

import (
"context"
"errors"
"strings"

"github.com/charmbracelet/huh"
)

var ErrInteractiveReviewAborted = errors.New("interactive review aborted by user")

// ConfirmPrompt is a generic confirmation prompt for planner-owned review flows.
type ConfirmPrompt struct {
Title string
Description []string
ConfirmLabel string
CancelLabel string
}

// PromptOption is one selectable value in a generic interactive prompt.
type PromptOption struct {
Label string
Value string
Description string
}

// SelectPrompt is a generic single-select prompt for planner-owned review flows.
type SelectPrompt struct {
Title string
Description []string
Options []PromptOption
}

// InteractivePrompter abstracts the concrete prompt implementation so review
// orchestration stays planner-owned and testable.
type InteractivePrompter interface {
Confirm(context.Context, ConfirmPrompt) error
Select(context.Context, SelectPrompt) (string, error)
}

// HuhPrompter implements InteractivePrompter using charmbracelet/huh forms.
type HuhPrompter struct{}

func (p *HuhPrompter) Confirm(ctx context.Context, prompt ConfirmPrompt) error {
confirmLabel := strings.TrimSpace(prompt.ConfirmLabel)
if confirmLabel == "" {
confirmLabel = "Continue"
}

cancelLabel := strings.TrimSpace(prompt.CancelLabel)
if cancelLabel == "" {
cancelLabel = "Abort"
}

var choice bool
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(strings.TrimSpace(prompt.Title)).
Description(promptDescription(prompt.Description)).
Affirmative(confirmLabel).
Negative(cancelLabel).
Value(&choice),
),
)

if err := form.RunWithContext(ctx); err != nil {
if errors.Is(err, huh.ErrUserAborted) {
return ErrInteractiveReviewAborted
}
return err
}

if !choice {
return ErrInteractiveReviewAborted
}

return nil
}

func (p *HuhPrompter) Select(ctx context.Context, prompt SelectPrompt) (string, error) {
options := make([]huh.Option[string], 0, len(prompt.Options))
for _, option := range prompt.Options {
label := option.Label
if description := strings.TrimSpace(option.Description); description != "" {
label += " - " + description
}
options = append(options, huh.NewOption(label, option.Value))
}
Comment thread
c-r33d marked this conversation as resolved.

var choice string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title(strings.TrimSpace(prompt.Title)).
Description(promptDescription(prompt.Description)).
Options(options...).
Value(&choice),
),
)

if err := form.RunWithContext(ctx); err != nil {
if errors.Is(err, huh.ErrUserAborted) {
return "", ErrInteractiveReviewAborted
}
return "", err
}

return choice, nil
}

func promptDescription(description []string) string {
lines := make([]string, 0, len(description))
for _, line := range description {
line = strings.TrimSpace(line)
if line == "" {
continue
}
lines = append(lines, line)
}

return strings.Join(lines, "\n")
}
Loading
Loading