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
18 changes: 5 additions & 13 deletions cmd/optiqor/color.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,15 @@ import (
"github.com/optiqor/optiqor-cli/internal/config"
)

// colorPolicyKey is the context key under which the resolved
// colour-policy boolean is stashed by the root command. Using a
// distinct unexported type prevents collisions with anything else
// the cobra command tree might tuck into the context.
// Unexported context-key types so cobra-tree values don't collide.
type colorPolicyKey struct{}

// withColorPolicy returns a context that carries the resolved
// "should we emit ANSI?" decision.
func withColorPolicy(ctx context.Context, useColor bool) context.Context {
return context.WithValue(ctx, colorPolicyKey{}, useColor)
}

// colorPolicyFrom recovers the colour-policy decision; defaults to
// false (plain) so subcommands that bypass the persistent pre-run
// fall back to safe-by-default plain output.
// colorPolicyFrom defaults to false (plain) so subcommands bypassing
// the persistent pre-run stay safe-by-default.
func colorPolicyFrom(ctx context.Context) bool {
if ctx == nil {
return false
Expand All @@ -32,16 +26,14 @@ func colorPolicyFrom(ctx context.Context) bool {
return v
}

// configKey is the context key for the loaded .optiqor.yaml.
type configKey struct{}

// withConfig stashes the loaded Config in ctx.
func withConfig(ctx context.Context, c config.Config) context.Context {
return context.WithValue(ctx, configKey{}, c)
}

// configFrom recovers the Config; returns the zero Config when none
// is set so callers don't need to nil-check.
// configFrom returns the zero Config when none is set so callers
// skip the nil-check.
func configFrom(ctx context.Context) config.Config {
if ctx == nil {
return config.Config{}
Expand Down
26 changes: 7 additions & 19 deletions cmd/optiqor/golden_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,10 @@ import (
"testing"
)

// -update regenerates the golden files from current output. Use
// sparingly: only after a deliberate UX change. The test diffs against
// the recorded output otherwise.
// -update regenerates the golden files. Use only after a deliberate
// UX change.
var update = flag.Bool("update", false, "update golden files")

// goldenDir holds the recorded outputs for stability tests. Adding a
// new test case is one fixture and one test entry — no hand-curating
// of expected strings.
const goldenDir = "../../testdata/golden"

type goldenCase struct {
Expand All @@ -40,9 +36,8 @@ func TestGolden(t *testing.T) {
t.Fatal(err)
}

// Pin terminal width so the boxed-card layout is deterministic
// across machines. Without this the developer's $COLUMNS leaks
// into golden output and CI (no TTY → fallback 80) diverges.
// Pin width or the dev's $COLUMNS leaks into goldens and diverges
// from CI (no TTY → fallback 80).
t.Setenv("COLUMNS", "80")

for _, tc := range goldenCases {
Expand All @@ -52,8 +47,7 @@ func TestGolden(t *testing.T) {
cmd.SetOut(&buf)
cmd.SetErr(&buf)
cmd.SetArgs(tc.args)
// Tests must run in the cmd/optiqor directory so the
// "../../testdata/..." paths resolve.
// Must run in cmd/optiqor so ../../testdata/... resolves.
_ = cmd.Execute()
got := normalize(buf.String())

Expand All @@ -75,14 +69,8 @@ func TestGolden(t *testing.T) {
}
}

// normalize replaces filesystem-dependent paths with stable placeholders
// so the golden file is portable across machines and CI runners.
//
// The analyze command resolves chart paths via filepath.Abs, which
// embeds the runner's home/workspace prefix in the report's "Source"
// field. We strip both the test's cwd and the repo root (one level
// up) so the golden output stays bit-identical between a developer's
// laptop and the GitHub Actions ubuntu-latest / macos-latest runners.
// normalize strips the test's cwd and the repo root from filepath.Abs
// output so goldens stay bit-identical across laptops and CI runners.
func normalize(s string) string {
if cwd, err := os.Getwd(); err == nil {
s = strings.ReplaceAll(s, cwd, "<CWD>")
Expand Down
98 changes: 38 additions & 60 deletions cmd/optiqor/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Command optiqor is the entrypoint for the open-source Optiqor CLI.
//
// The CLI is a deterministic rule engine that analyzes Helm charts for cost
// inefficiencies. It also flags obvious security misconfigurations as a bonus
// side-effect of parsing. It does NOT call any LLM and does NOT phone home by
// default — see ../../CLAUDE.md for the hard rules.
// Command optiqor is the open-source CLI entrypoint. Deterministic
// rule engine — no LLM, no telemetry by default. See ../../CLAUDE.md
// for the hard rules.
package main

import (
Expand Down Expand Up @@ -39,6 +36,8 @@ var errFindings = errors.New("optiqor: findings exceed threshold")

var version = "dev"

// accuracyDisclosure is the mandatory line every command's help and
// output must contain (hard rule per CLAUDE.md).
const accuracyDisclosure = "Sandbox accuracy: ±40%. Install the Optiqor agent for exact numbers (optiqor.dev/get)."

func main() {
Expand All @@ -47,7 +46,7 @@ func main() {
case err == nil:
os.Exit(exitSuccess)
case errors.Is(err, errFindings):
// Already-rendered finding output; suppress an additional error line.
// Findings already rendered; suppress an extra error line.
os.Exit(exitFindings)
default:
printError(os.Stderr, err)
Expand Down Expand Up @@ -92,14 +91,12 @@ namespaces, etc.). Cost is the headline; security is a side-effect.
root.PersistentFlags().BoolVar(&noColor, "no-color", false, "disable colored output (also: NO_COLOR env)")
root.PersistentFlags().StringVar(&configPath, "config", "", "path to .optiqor.yaml (default: ./.optiqor.yaml or $OPTIQOR_CONFIG)")

// Stash the no-color decision and the loaded config in context so
// subcommands can read both.
root.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
cfg, err := config.Load(configPath)
if err != nil {
return err
}
// Config-level no_color implies --no-color unless flag explicitly disabled.
// Config-level no_color implies --no-color.
effectiveNoColor := noColor || cfg.NoColor
ctx := cmd.Context()
ctx = withColorPolicy(ctx, resolveColor(cmd, effectiveNoColor))
Expand All @@ -123,7 +120,6 @@ namespaces, etc.). Cost is the headline; security is a side-effect.
return root
}

// versionTemplate prints a polished one-liner including the brand.
func versionTemplate() string {
return fmt.Sprintf("optiqor %s — %s\n", version, "Helm chart cost analysis (security bonus)")
}
Expand Down Expand Up @@ -168,7 +164,7 @@ side-effect of parsing — they are not the headline feature.
return err
}

// Merge config-file defaults with flags. Flags win when supplied.
// Flags win over config when supplied.
cfg := configFrom(cmd.Context())
effSev := minSev
if effSev == "" {
Expand All @@ -194,8 +190,7 @@ side-effect of parsing — they are not the headline feature.
if err := writeHTMLReport(htmlPath, rep); err != nil {
return err
}
// --html is a side-channel: text/JSON still prints to
// stdout so users get the terminal report AND a file.
// --html is side-channel; stdout still gets text/JSON.
}
if err := emitReport(cmd, rep, jsonOut, outputPath, roast); err != nil {
return err
Expand All @@ -219,10 +214,9 @@ side-effect of parsing — they are not the headline feature.
return cmd
}

// writeHTMLReport renders rep through pkg/htmlrender into the file at
// path. The same package is consumed by the backend's share-page
// handler so the local file and the optiqor.dev/r/<hash> page render
// byte-identically.
// writeHTMLReport: pkg/htmlrender is also consumed by the backend's
// share-page handler so the local file and optiqor.dev/r/<hash>
// render byte-identically.
func writeHTMLReport(path string, rep render.Report) error {
f, err := os.Create(path) //nolint:gosec // user-supplied output path
if err != nil {
Expand All @@ -237,12 +231,9 @@ func writeHTMLReport(path string, rep render.Report) error {
})
}

// emitReport renders the report in JSON or styled text. When
// outputPath is non-empty the rendered bytes go to that file instead
// of stdout (CI use case: `optiqor analyze --json --output result.json`).
// The roast flag swaps the brand tagline and footer quip for the
// `--roast` variants; finding titles are roasted upstream by the
// analyze command before the report reaches here.
// emitReport writes JSON or styled text. outputPath redirects to a
// file (CI use case); roast swaps tagline/footer — finding titles
// are roasted upstream in the analyze command.
func emitReport(cmd *cobra.Command, rep render.Report, jsonOut bool, outputPath string, roast bool) error {
w, closeFn, err := openOutput(cmd, outputPath)
if err != nil {
Expand All @@ -259,9 +250,9 @@ func emitReport(cmd *cobra.Command, rep render.Report, jsonOut bool, outputPath
return render.Text(w, rep, opts)
}

// openOutput resolves the destination: stdout when path is empty;
// otherwise creates / truncates the file. The returned closer is a
// no-op for stdout so callers can defer it unconditionally.
// openOutput returns stdout when path is empty, otherwise opens the
// file. The closer is a no-op for stdout so callers defer it
// unconditionally.
func openOutput(cmd *cobra.Command, path string) (io.Writer, func(), error) {
if path == "" {
return cmd.OutOrStdout(), func() {}, nil
Expand All @@ -273,22 +264,15 @@ func openOutput(cmd *cobra.Command, path string) (io.Writer, func(), error) {
return f, func() { _ = f.Close() }, nil
}

// emitShareURL handles the `--share` flag end-to-end.
//
// It computes the local content-addressable hash, attempts to upload
// the sanitised payload to the sandbox endpoint, and prints the
// resulting `optiqor.dev/r/<hash>` URL to stderr (so JSON/text output on
// stdout stays clean).
//
// The function never blocks the caller's success path — if the upload
// fails (offline, sandbox down, 5xx), we still print the URL so the
// user has a stable identifier they can re-share later. The endpoint
// is overridable via OPTIQOR_SHARE_URL for self-hosted deploys.
// emitShareURL handles --share end-to-end. Prints to stderr so
// stdout (JSON/text) stays clean. Never blocks the success path — on
// upload failure we still print the URL so the user has a stable
// identifier to re-share later. OPTIQOR_SHARE_URL overrides the
// endpoint for self-hosted deploys.
func emitShareURL(cmd *cobra.Command, rep any) {
endpoint := os.Getenv("OPTIQOR_SHARE_URL")
res := share.Upload(rep, endpoint)
if res.Hash == "" {
// Hash failed entirely — nothing to print.
return
}
suffix := ""
Expand All @@ -301,7 +285,7 @@ func emitShareURL(cmd *cobra.Command, rep any) {
}

// checkFailOn returns errFindings when any finding meets or exceeds
// the threshold severity. Empty threshold is a no-op.
// threshold. Empty threshold is a no-op.
func checkFailOn(rep render.Report, threshold string) error {
if threshold == "" {
return nil
Expand Down Expand Up @@ -346,9 +330,8 @@ func toUpper(s string) string {
return string(out)
}

// demoChart is the bundled demo values file. //go:embed lets us ship
// the fixture inside the binary so `npx @optiqor/cli demo` works with
// no input.
// demoChart ships inside the binary so `npx @optiqor/cli demo` works
// with no input.
//
//go:embed demo/values.yaml
var demoChart []byte
Expand Down Expand Up @@ -385,19 +368,17 @@ func newDemoCmd() *cobra.Command {
return cmd
}

// renderOpts builds a render.Options for the active command, picking up
// the colour-policy decision the persistent pre-run stashed in context.
// renderOpts picks up the colour-policy decision stashed by the
// persistent pre-run.
func renderOpts(cmd *cobra.Command) render.Options {
return render.Options{
Color: colorPolicyFrom(cmd.Context()),
Width: terminalWidth(),
}
}

// renderOptsRoast extends renderOpts with the roast-mode strings so
// the renderer prints the playful tagline + footer quip without
// importing the roast package itself. Findings are roasted upstream
// in the analyze command (see internal/roast).
// renderOptsRoast supplies the playful strings so render stays
// unaware of internal/roast.
func renderOptsRoast(cmd *cobra.Command) render.Options {
o := renderOpts(cmd)
o.Roast = true
Expand All @@ -406,14 +387,13 @@ func renderOptsRoast(cmd *cobra.Command) render.Options {
return o
}

// resolveColor decides whether to emit ANSI for a given command.
// Order of precedence (highest to lowest):
// resolveColor decides whether to emit ANSI. Precedence:
//
// 1. --no-color flag
// 2. NO_COLOR env var (any non-empty value, per https://no-color.org)
// 3. CLICOLOR_FORCE=1 forces color even when not a TTY
// 4. stdout is a TTY → color on
// 5. otherwise → color off
// 2. NO_COLOR (per https://no-color.org)
// 3. CLICOLOR_FORCE=1 forces colour even when not a TTY
// 4. stdout is a TTY → on
// 5. otherwise → off
func resolveColor(cmd *cobra.Command, noColor bool) bool {
if noColor {
return false
Expand All @@ -431,8 +411,7 @@ func resolveColor(cmd *cobra.Command, noColor bool) bool {
return style.IsTTY(out)
}

// terminalWidth returns the current terminal width (cols). Falls back
// to 80 when not a TTY or when reading $COLUMNS fails.
// terminalWidth returns $COLUMNS or 80.
func terminalWidth() int {
if v := os.Getenv("COLUMNS"); v != "" {
if n, err := atoi(v); err == nil && n > 20 {
Expand All @@ -453,7 +432,7 @@ func atoi(s string) (int, error) {
return n, nil
}

// printError renders an error in red on a TTY; plain on a pipe.
// printError renders in red on a TTY; plain on a pipe.
func printError(w io.Writer, err error) {
if err == nil {
return
Expand All @@ -466,8 +445,7 @@ func printError(w io.Writer, err error) {
_, _ = fmt.Fprintln(w, t.SevHigh.Render(" ERROR ")+" "+err.Error())
}

// bytesReader is a tiny adapter so analyze.Run can read from a byte slice
// without pulling in bytes.NewReader at the import-graph root of main.
// bytesReader avoids pulling bytes.NewReader into the main import graph.
func bytesReader(b []byte) *bytesReaderImpl { return &bytesReaderImpl{b: b} }

type bytesReaderImpl struct {
Expand Down
19 changes: 6 additions & 13 deletions cmd/optiqor/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"testing"
)

// TestRoot_Help just exercises the top-level cobra wiring; ensures we
// can build and serialise the help text without panicking.
func TestRoot_Help(t *testing.T) {
cmd := newRootCmd()
var buf bytes.Buffer
Expand All @@ -30,7 +28,6 @@ func TestRoot_Help(t *testing.T) {
}
}

// TestVersion_Output checks the polished version line.
func TestVersion_Output(t *testing.T) {
cmd := newRootCmd()
var buf bytes.Buffer
Expand All @@ -47,7 +44,8 @@ func TestVersion_Output(t *testing.T) {
}

// TestDemo_RunsAndIncludesDisclosure exercises the full demo path
// (embedded fixture → parser → rules → render).
// (embedded fixture → parser → rules → render) and checks the
// accuracy disclosure shows up.
func TestDemo_RunsAndIncludesDisclosure(t *testing.T) {
cmd := newRootCmd()
var buf bytes.Buffer
Expand Down Expand Up @@ -75,10 +73,8 @@ func TestDemo_RunsAndIncludesDisclosure(t *testing.T) {
}
}

// TestAnalyze_FixtureFile exercises the analyze command against the
// versioned testdata fixture. Asserts the well-known severities and
// detectors fire; lets the count grow naturally as the detector
// library expands.
// TestAnalyze_FixtureFile asserts well-known severities and
// detectors fire; lets count grow naturally as the library expands.
func TestAnalyze_FixtureFile(t *testing.T) {
cmd := newRootCmd()
var buf bytes.Buffer
Expand All @@ -89,9 +85,8 @@ func TestAnalyze_FixtureFile(t *testing.T) {
t.Fatalf("execute analyze: %v\n%s", err, buf.String())
}
out := buf.String()
// Severity badges + workload names appear on the per-finding line;
// the renderer prints them as bare identifiers rather than as
// "workload: <name>".
// Renderer emits severity badges and bare workload identifiers
// (no "workload: <name>" prefix).
for _, want := range []string{
"15 workloads",
"HIGH",
Expand All @@ -111,8 +106,6 @@ func TestAnalyze_FixtureFile(t *testing.T) {
}
}

// TestAnalyze_JSONShape exercises --json on a fixture and validates
// the schema is intact.
func TestAnalyze_JSONShape(t *testing.T) {
cmd := newRootCmd()
var buf bytes.Buffer
Expand Down
Loading
Loading