feat: add interactive policy-file wizard (kosli create policy-file)#766
feat: add interactive policy-file wizard (kosli create policy-file)#766dangrondahl wants to merge 52 commits intomainfrom
Conversation
|
Claude finished @dangrondahl's task in 5m 30s —— View job Review of PR #766:
|
|
Addressed 5 of the 7 review comments in commits
|
|
Reviewed the latest 5 inline comments — all are informational observations, none blocking:
No changes needed for these. |
f1d03a2 to
048b2a7
Compare
These operators need special syntax (list literals, function form) that ComparisonExpr doesn't produce. They already have dedicated wizard paths (flow name match, artifact name regex). Replace with numeric comparison operators that work correctly with the quoted-value format.
The matches operator needs function syntax matches(ctx, "pattern") rather than infix syntax. Extract MatchesExpr and use it when the custom comparison operator is "matches". Also replace "in" with numeric comparison operators that work with quoted values.
Add exists() as an operator choice in the custom comparison wizard path.
Unlike other operators, exists() takes only a context field and no value.
Generates ${{ exists(field) }} syntax per the policy reference docs.
After building a sub-expression, the wizard now asks whether to negate
it (not) and whether to combine it with another condition (and/or).
This allows building compound expressions like:
${{ flow.name == "prod" and not(matches(artifact.name, "^datadog:.*")) }}
New steps: stepExprNegate, stepExprCombineConfirm, stepExprCombineOp.
Sub-expressions accumulate in pendingExprs and are combined on finalize.
Change NegateExpr to produce "not expr" instead of "not(expr)" to match the policy expression language syntax.
Use term.GetSize to read the actual terminal width, falling back to 80. This prevents a brief layout glitch on narrow terminals before the first WindowSizeMsg arrives from bubbletea.
Handle expressions like ${{expr}} (no inner spaces) in addition to
the standard ${{ expr }} format.
Catch invalid regex patterns (e.g. [unclosed) at input time rather than letting them silently end up in the policy file.
The go.mod conflict resolution during rebase accidentally downgraded kind from v0.31.0 to v0.11.1, breaking ExportKubeConfig API.
not flow.name == "prod" could be parsed as (not flow.name) == "prod". Using not (flow.name == "prod") is unambiguous.
Without clearing, a failed matches regex validation would leave validationErr set, causing an infinite loop when the user retried with a valid regex or switched to a different operator.
Empty values produced expressions like ${{ flow.name == "" }} which
are technically valid but likely unintended. The exists operator
correctly allows empty values since it takes no argument.
Move the os.Stat existence check into validateYAMLExtension so users get immediate feedback at stepSaveFile time, avoiding lost work if the target file already exists.
5ad8b6e to
aaa321f
Compare
| func extractFormValues(f *huh.Form) formValues { | ||
| return formValues{ | ||
| confirm: f.GetBool("confirm"), | ||
| str: firstNonEmpty(f.GetString("value"), f.GetString("filename"), f.GetString("mode")), |
There was a problem hiding this comment.
Nit: document the firstNonEmpty invariant
This was flagged in prior reviews. Each form step uses at most one of "value", "filename", or "mode" as its primary string key — the implicit coupling here is easy for a future contributor to break. A one-line comment would make this self-documenting:
| str: firstNonEmpty(f.GetString("value"), f.GetString("filename"), f.GetString("mode")), | |
| func extractFormValues(f *huh.Form) formValues { | |
| // Each form step uses at most one of "value", "filename", or "mode" as its | |
| // primary string key. firstNonEmpty picks the one that was set. | |
| return formValues{ |
| huh.NewSelect[string]().Key("op").Title("Operator"). | ||
| Options(huh.NewOptions("==", "!=", ">", "<", ">=", "<=", "matches", "exists")...), | ||
| huh.NewInput().Key("value").Title("Value"). | ||
| Description("The value to compare against (ignored for exists)"), |
There was a problem hiding this comment.
Low: Value input is live but ignored when exists is selected
The description (ignored for exists) is good, but users can still type into the field and expect it to matter. Consider adding a Placeholder("(not used)") to reinforce the message, or validating that the field is empty when exists is selected. Not blocking, but would improve clarity.
| outPath := filepath.Clean(wm.OutputFile) | ||
| if err := validateOutputFile(outPath); err != nil { | ||
| return err |
There was a problem hiding this comment.
Minor: dual-location validation is a bit redundant now
Since validateYAMLExtension in forms.go:268 now checks os.Stat too, the user gets file-exists feedback at wizard time. This validateOutputFile still validates extension + existence again after the wizard. The extension check is guaranteed to pass (the form already validated it), and the existence check is only useful in the TOCTOU gap. This is fine as a safety net, but worth noting the redundancy — the post-wizard check now primarily protects against concurrent file creation.
Why
Writing Kosli environment policy files by hand is harder than it should be. The YAML format, exception syntax, expression operators (
==,!=,matches(),exists(),not,and,or), and available context fields (flow.name,flow.tags.<key>,artifact.name,artifact.fingerprint) are easy to forget — especially when you're creating your first policy or haven't touched one in a while.This is an attempt to lower the barrier to creating the first (and many subsequent) policy files by providing a guided, interactive experience.
Summary
kosli create policy-filecommand that launches an interactive TUI wizard to build environment policy YAML files conforming to the policy schema${{ }}syntax) with support for:matches())exists())and,or,not).yaml/.ymlextension required, no silent overwritesNew packages
internal/policyinternal/policywizardDependencies added
charmbracelet/huh— form componentscharmbracelet/bubbletea— TUI frameworkcharmbracelet/lipgloss— stylingcharmbracelet/bubbles— spinner