diff --git a/cmd/crossplane/main.go b/cmd/crossplane/main.go index ba73e06c..f4b4c5d3 100644 --- a/cmd/crossplane/main.go +++ b/cmd/crossplane/main.go @@ -51,8 +51,8 @@ import ( "github.com/crossplane/cli/v2/cmd/crossplane/xr" "github.com/crossplane/cli/v2/cmd/crossplane/xrd" "github.com/crossplane/cli/v2/internal/config" + "github.com/crossplane/cli/v2/internal/exitcode" "github.com/crossplane/cli/v2/internal/maturity" - isimulate "github.com/crossplane/cli/v2/internal/simulate" "github.com/crossplane/cli/v2/internal/terminal" clixpkg "github.com/crossplane/cli/v2/internal/xpkg" @@ -122,7 +122,7 @@ func main() { flagVal, err := configFlag(os.Args[1:]) if err != nil { fmt.Fprintf(os.Stderr, "crossplane: %v\n", err) - os.Exit(isimulate.ExitCodeUsage) + os.Exit(exitcode.ExitCodeUsage) } cfgPath := config.ResolvePath(flagVal) @@ -181,7 +181,7 @@ func main() { stop() // A silent ExitError is a result, such as changes found, not an error, // so it exits with its code and prints nothing. - code, silent := isimulate.SilentExit(err) + code, silent := exitcode.SilentExit(err) if interrupted { // Exit with the conventional 128+SIGINT status. if err != nil && !silent { diff --git a/cmd/crossplane/project/integration_plan_output_test.go b/cmd/crossplane/project/integration_plan_output_test.go index dbed8e77..3c312ca5 100644 --- a/cmd/crossplane/project/integration_plan_output_test.go +++ b/cmd/crossplane/project/integration_plan_output_test.go @@ -31,6 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" dp "github.com/crossplane/cli/v2/internal/diff/diffprocessor" + "github.com/crossplane/cli/v2/internal/exitcode" "github.com/crossplane/cli/v2/internal/plan" "github.com/crossplane/cli/v2/internal/simulate" "github.com/crossplane/cli/v2/internal/testutils/integration" @@ -486,7 +487,7 @@ func TestSimulateNameRejected(t *testing.T) { reason: "namespace/name with --namespace is ambiguous and is rejected.", args: []string{"--namespace=default", "--name=default/test-resource"}, wantErr: "can't be combined with --namespace", - wantCode: simulate.ExitCodeUsage, + wantCode: exitcode.ExitCodeUsage, }, "NamePartiallyUnmatchedFailsBeforeRendering": { reason: "one --name no composite matches fails the run, and no plan is rendered.", diff --git a/cmd/crossplane/simulate/simulate.go b/cmd/crossplane/simulate/simulate.go index 655ab87f..e2d7179f 100644 --- a/cmd/crossplane/simulate/simulate.go +++ b/cmd/crossplane/simulate/simulate.go @@ -38,6 +38,7 @@ import ( k8 "github.com/crossplane/cli/v2/internal/diff/client/kubernetes" "github.com/crossplane/cli/v2/internal/diffserver" "github.com/crossplane/cli/v2/internal/docker" + "github.com/crossplane/cli/v2/internal/exitcode" "github.com/crossplane/cli/v2/internal/kube" "github.com/crossplane/cli/v2/internal/plan" isimulate "github.com/crossplane/cli/v2/internal/simulate" @@ -155,7 +156,7 @@ func (c *Cmd) Run(ctx context.Context, k *kong.Context, logger logging.Logger, s } // exitStatus ranks the plans' outcome: an error, which exits 1, when any -// plan errored; otherwise a silent isimulate.ExitError that exits 3 when a plan +// plan errored; otherwise a silent exitcode.ExitError that exits 3 when a plan // would change anything; otherwise nil. func exitStatus(plans []*plan.ResourcePlan) error { s := plan.Summarize(plans) @@ -163,7 +164,7 @@ func exitStatus(plans []*plan.ResourcePlan) error { return errors.Errorf("cannot plan %d of %d resources; see their errors in the output", s.Errored, len(plans)) } if s.Changes() > 0 { - return isimulate.ExitError{Code: isimulate.ExitCodeChanges} + return exitcode.ExitError{Code: exitcode.ExitCodeChanges} } return nil } diff --git a/cmd/crossplane/simulate/simulate_test.go b/cmd/crossplane/simulate/simulate_test.go index ceba17b5..f74689bf 100644 --- a/cmd/crossplane/simulate/simulate_test.go +++ b/cmd/crossplane/simulate/simulate_test.go @@ -21,8 +21,8 @@ import ( "github.com/google/go-cmp/cmp" + "github.com/crossplane/cli/v2/internal/exitcode" "github.com/crossplane/cli/v2/internal/plan" - isimulate "github.com/crossplane/cli/v2/internal/simulate" ) func TestExitStatus(t *testing.T) { @@ -61,7 +61,7 @@ func TestExitStatus(t *testing.T) { // other error with 1. got := 0 err := exitStatus(tc.args.plans) - if code, ok := isimulate.SilentExit(err); ok { + if code, ok := exitcode.SilentExit(err); ok { got = code } else if err != nil { got = 1 diff --git a/internal/simulate/exit.go b/internal/exitcode/exit.go similarity index 94% rename from internal/simulate/exit.go rename to internal/exitcode/exit.go index ad0b5564..e051e146 100644 --- a/internal/simulate/exit.go +++ b/internal/exitcode/exit.go @@ -14,17 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -package simulate +// Package exitcode holds the exit statuses and the ExitError the commands +// share. +package exitcode import "errors" -// Exit statuses the simulate commands share. +// Exit statuses the commands share. const ( // ExitCodeUsage is the status kong exits with on a parse error. Kong does // not export it, so it is mirrored here for the flag errors main finds // before Parse. ExitCodeUsage = 80 - // ExitCodeSchemaValidation means only schema validation failed. + // ExitCodeSchemaValidation means only schema validation failed in a + // simulate command. ExitCodeSchemaValidation = 2 // ExitCodeChanges is the status a simulate command exits with when the // simulation succeeded and found changes. diff --git a/internal/simulate/exit_test.go b/internal/exitcode/exit_test.go similarity index 99% rename from internal/simulate/exit_test.go rename to internal/exitcode/exit_test.go index 5eed6fa8..8f690968 100644 --- a/internal/simulate/exit_test.go +++ b/internal/exitcode/exit_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package simulate +package exitcode import ( "testing" diff --git a/internal/simulate/annotate.go b/internal/simulate/annotate.go index 3c1c4ea0..41430359 100644 --- a/internal/simulate/annotate.go +++ b/internal/simulate/annotate.go @@ -367,7 +367,7 @@ const probe = "CROSSPLANE-PROBE-7f3c" // are the field's, because sigsyaml writes keys sorted, so the lines before // and after a value don't depend on it, and the line diff keeps lines whole. // -// ponytail: a field is found only where setPath reaches it, so a keyed +// NOTE: a field is found only where setPath reaches it, so a keyed // item or index the desired side lacks gets no mark in a modified diff, and a // keyed segment matching several items marks the first "+" line from the // first match to the last. The header, the summary and -o plan still say diff --git a/internal/simulate/flags.go b/internal/simulate/flags.go index 410ddf7a..1e727fa8 100644 --- a/internal/simulate/flags.go +++ b/internal/simulate/flags.go @@ -19,34 +19,23 @@ package simulate import ( "time" - un "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" k8stypes "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" "github.com/crossplane/crossplane-runtime/v2/pkg/errors" dp "github.com/crossplane/cli/v2/internal/diff/diffprocessor" - "github.com/crossplane/cli/v2/internal/diffserver" "github.com/crossplane/cli/v2/internal/kube" xprender "github.com/crossplane/cli/v2/pkg/render" ) -// Flags are the flags, and the run state, of a simulate command. +// Flags are the flags of a simulate command. type Flags struct { // EngineFlags select the crossplane render backend, as for render. Its // SessionID labels the diff server containers; the render's containers // are labelled with the run's own ID, which reclaims them. xprender.EngineFlags `prefix:"" set:"session_id_help=Session ID to apply as the simulate.crossplane.io/session label on diff server containers. Defaults to a random per-process ID. Ignored with --skip-plan."` - // Run sets these; nil with --skip-plan (servers also when Hooks.Router is set). - xrPlanner *XRPlanner - // keep is the equal-diff filter the engine and xrPlanner share. - keep func(*un.Unstructured) bool - servers *diffserver.Manager - // noServersNoted is set once the "no diff servers" line is printed. - noServersNoted bool - // sel is Inputs.Selection, which Run copies in; zero without one. - sel CompositionFlags CacheDir string `env:"CROSSPLANE_XPKG_CACHE" help:"Directory for cached xpkg package contents (functions and providers)." name:"cache-dir"` Context string `help:"Kubernetes context to simulate against. Defaults to the current context." predictor:"context" short:"c"` diff --git a/internal/simulate/run.go b/internal/simulate/run.go index f442d656..8d155f9e 100644 --- a/internal/simulate/run.go +++ b/internal/simulate/run.go @@ -53,6 +53,7 @@ import ( dt "github.com/crossplane/cli/v2/internal/diff/result" "github.com/crossplane/cli/v2/internal/diffserver" "github.com/crossplane/cli/v2/internal/docker" + "github.com/crossplane/cli/v2/internal/exitcode" "github.com/crossplane/cli/v2/internal/kube" "github.com/crossplane/cli/v2/internal/project" "github.com/crossplane/cli/v2/internal/terminal" @@ -79,7 +80,7 @@ type Packages struct { // only ICs and Client. Run stops c.servers when it is set. Planning // shares the engine's clients' REST mapper and CRD cache, so a run discovers // the cluster once and fetches each CRD at most once. -func (c *Flags) planOptions(sigCtx context.Context, k *kong.Context, logger logging.Logger, sp terminal.SpinnerPrinter, restCfg *rest.Config, clients *client.Clients, ns string, pkgs *Packages) ([]dp.ProcessorOption, error) { +func (c *runner) planOptions(sigCtx context.Context, k *kong.Context, logger logging.Logger, sp terminal.SpinnerPrinter, restCfg *rest.Config, clients *client.Clients, ns string, pkgs *Packages) ([]dp.ProcessorOption, error) { rmapper, crds := clients.Core.Mapper, clients.K8sClients.Schema kubeClient, err := NewKubeClient(restCfg, rmapper) if err != nil { @@ -157,7 +158,7 @@ func providerSource(providers string, project bool) source { // noteNoServers prints NoteNoDiffServers to stderr once, for -o // diff, which has no notes; the other formats carry it in their notes. -func (c *Flags) noteNoServers(k *kong.Context, notes []string) { +func (c *runner) noteNoServers(k *kong.Context, notes []string) { if c.noServersNoted || c.Output != string(renderer.OutputFormatDiff) || !slices.Contains(notes, NoteNoDiffServers) { return } @@ -214,7 +215,7 @@ func changedFunctions(ctx context.Context, rc k8client.ResourceClient, digests m // diffCompositions returns the diff simulate runs for Compositions: their // impact on the composite resources in scope, rendered to k's writers. // Errors are wrapped with msg. -func (c *Flags) diffCompositions(k *kong.Context, logger logging.Logger, proc dp.CompDiffProcessor, comps []*un.Unstructured, msg string) func(context.Context) (bool, error) { +func (c *runner) diffCompositions(k *kong.Context, logger logging.Logger, proc dp.CompDiffProcessor, comps []*un.Unstructured, msg string) func(context.Context) (bool, error) { return func(ctx context.Context) (bool, error) { output, err := proc.DiffComposition(ctx, comps, c.sel.ns, c.sel.refs) @@ -234,7 +235,7 @@ func (c *Flags) diffCompositions(k *kong.Context, logger logging.Logger, proc dp // crossplane-diff's xr command. A claim is simulated as its XR, but the // report names the input, so its result is listed under the claim's name // (crossplane/cli#367). -func (c *Flags) diffComposites(k *kong.Context, logger logging.Logger, proc dp.DiffProcessor, xp *xpclient.Clients, ns string, xrs []*un.Unstructured) func(context.Context) (bool, error) { +func (c *runner) diffComposites(k *kong.Context, logger logging.Logger, proc dp.DiffProcessor, xp *xpclient.Clients, ns string, xrs []*un.Unstructured) func(context.Context) (bool, error) { return func(ctx context.Context) (bool, error) { if err := checkComposites(ctx, xp.Definition, ns, xrs); err != nil { return false, err @@ -275,7 +276,7 @@ type processor interface { // exits 3; one that fails only schema validation returns one that exits 2. // ctx must not be cancelled by its owner until simulate returns, so a // cancellation reads as an interrupt. -func (c *Flags) simulate(ctx context.Context, logger logging.Logger, initClients func(context.Context, logging.Logger) error, proc processor, diff func(context.Context) (bool, error)) (err error) { +func (c *runner) simulate(ctx context.Context, logger logging.Logger, initClients func(context.Context, logging.Logger) error, proc processor, diff func(context.Context) (bool, error)) (err error) { // An interrupted run reports the interruption, not the cancelled calls it // caused. main exits 130 for it. defer func() { @@ -299,10 +300,10 @@ func (c *Flags) simulate(ctx context.Context, logger logging.Logger, initClients hasDiffs, err := diff(ctx) switch dp.DetermineExitCode(err, hasDiffs) { case dp.ExitCodeSchemaValidation: - return ExitError{Err: err, Code: ExitCodeSchemaValidation} + return exitcode.ExitError{Err: err, Code: exitcode.ExitCodeSchemaValidation} case dp.ExitCodeDiffDetected: // Changes found are a result, not an error, so nothing is printed. - return ExitError{Code: ExitCodeChanges} + return exitcode.ExitError{Code: exitcode.ExitCodeChanges} } return err @@ -342,7 +343,7 @@ func (c *CompositionFlags) scope() (string, []k8stypes.NamespacedName, error) { // XR and composition processors get the same options, so they share one advisory collector. fns are the project's // functions, of which embedded are the locally built ones, and ics rewrite // the other functions' images. -func (c *Flags) processorOptions(k *kong.Context, logger logging.Logger, creds []corev1.Secret, fns []pkgv1.Function, embedded map[string]name.Digest, ics []pkgv1beta1.ImageConfig) []dp.ProcessorOption { +func (c *runner) processorOptions(k *kong.Context, logger logging.Logger, creds []corev1.Secret, fns []pkgv1.Function, embedded map[string]name.Digest, ics []pkgv1beta1.ImageConfig) []dp.ProcessorOption { c.SetDefaultCrossplaneDockerNetwork(fns) opts := []dp.ProcessorOption{ @@ -416,7 +417,7 @@ func functionProviderFactory(fns []pkgv1.Function, embedded map[string]name.Dige // writing to k's stdout and stderr. The processors return reports; the command // renders them with these options. Diffs are colorized only when a person // reads them: on a terminal, or as if on one under the test hooks. -func (c *Flags) diffOptions(k *kong.Context) renderer.DiffOptions { +func (c *runner) diffOptions(k *kong.Context) renderer.DiffOptions { opts := renderer.DefaultDiffOptions() opts.UseColors = !c.NoColor && (c.Hooks != nil && c.Hooks.Terminal || isTerminal(k.Stdout)) opts.Compact = c.Compact @@ -427,7 +428,7 @@ func (c *Flags) diffOptions(k *kong.Context) renderer.DiffOptions { // providerPlans returns the provider plans the run collected, or nil when // planning was skipped. -func (c *Flags) providerPlans() *ProviderPlans { +func (c *runner) providerPlans() *ProviderPlans { if c.xrPlanner == nil { return nil } @@ -447,7 +448,7 @@ func planNotes(skipPlan, planned bool, routed, considered int) []string { return nil } -func (c *Flags) notes() []string { +func (c *runner) notes() []string { if c.xrPlanner == nil { return planNotes(c.SkipPlan, false, 0, 0) } @@ -456,7 +457,7 @@ func (c *Flags) notes() []string { // renderXRReport writes an xr run's report in the format -o selects, as // renderReport does. -func (c *Flags) renderXRReport(k *kong.Context, logger logging.Logger, r *dt.XRReport, runErr error) (bool, error) { +func (c *runner) renderXRReport(k *kong.Context, logger logging.Logger, r *dt.XRReport, runErr error) (bool, error) { plans := c.providerPlans() return c.renderReport(k, logger, runErr, FromXRReport(r, plans, c.keep), r.HasChanges(), dt.XRDryRunWarnings(r.Groups), func(opts renderer.DiffOptions) error { // Annotated even without plans, so a Secret's values are redacted. @@ -467,7 +468,7 @@ func (c *Flags) renderXRReport(k *kong.Context, logger logging.Logger, r *dt.XRR // renderCompReport writes a composition run's report in the format -o // selects, as renderReport does. -func (c *Flags) renderCompReport(k *kong.Context, logger logging.Logger, o *dt.CompDiffOutput, runErr error) (bool, error) { +func (c *runner) renderCompReport(k *kong.Context, logger logging.Logger, o *dt.CompDiffOutput, runErr error) (bool, error) { plans := c.providerPlans() return c.renderReport(k, logger, runErr, FromCompReport(o, plans, c.keep), o.HasChanges(), dt.CompDryRunWarnings(o), func(opts renderer.DiffOptions) error { opts.MinimizeComposition = c.sel.MinimizeComposition @@ -482,7 +483,7 @@ func (c *Flags) renderCompReport(k *kong.Context, logger logging.Logger, o *dt.C // whether the run changed anything, provider plans included, and runErr // with any render failure joined to it, so neither is lost. changed is // whether the report itself has changes. -func (c *Flags) renderReport(k *kong.Context, logger logging.Logger, runErr error, res Result, changed bool, warnings []dt.OutputWarning, renderDiff func(renderer.DiffOptions) error) (bool, error) { +func (c *runner) renderReport(k *kong.Context, logger logging.Logger, runErr error, res Result, changed bool, warnings []dt.OutputWarning, renderDiff func(renderer.DiffOptions) error) (bool, error) { opts := c.diffOptions(k) plans := c.providerPlans() if plans != nil { @@ -512,7 +513,7 @@ func (c *Flags) renderReport(k *kong.Context, logger logging.Logger, runErr erro // writes derived, the warnings derived from the report, to stderr, as the // engine's renderers do. The run's other warnings went to stderr when they // were raised. -func (c *Flags) renderPlan(k *kong.Context, res Result, derived []dt.OutputWarning) error { +func (c *runner) renderPlan(k *kong.Context, res Result, derived []dt.OutputWarning) error { r, err := NewRenderer(c.Output) if err != nil { return err @@ -556,6 +557,27 @@ func isTerminal(w io.Writer) bool { // or composite resource or claim files, with the cluster's Compositions and // functions. func (c *Flags) Run(sigCtx context.Context, k *kong.Context, logger logging.Logger, sp terminal.SpinnerPrinter, in *Inputs) error { + return (&runner{Flags: c}).run(sigCtx, k, logger, sp, in) +} + +// runner is the state of one Run: the flags it was configured by, and what +// planning sets up along the way. +type runner struct { + *Flags + + // xrPlanner and servers are set by planOptions; nil with --skip-plan + // (servers also when Hooks.Router is set). + xrPlanner *XRPlanner + // keep is the equal-diff filter the engine and xrPlanner share. + keep func(*un.Unstructured) bool + servers *diffserver.Manager + // noServersNoted is set once the "no diff servers" line is printed. + noServersNoted bool + // sel is Inputs.Selection, which run copies in; zero without one. + sel CompositionFlags +} + +func (c *runner) run(sigCtx context.Context, k *kong.Context, logger logging.Logger, sp terminal.SpinnerPrinter, in *Inputs) error { if in.Selection != nil { c.sel = *in.Selection } diff --git a/internal/simulate/run_test.go b/internal/simulate/run_test.go index f65061e0..7021d3f3 100644 --- a/internal/simulate/run_test.go +++ b/internal/simulate/run_test.go @@ -51,6 +51,7 @@ import ( dt "github.com/crossplane/cli/v2/internal/diff/result" "github.com/crossplane/cli/v2/internal/diff/testutils" difftypes "github.com/crossplane/cli/v2/internal/diff/types" + "github.com/crossplane/cli/v2/internal/exitcode" "github.com/crossplane/cli/v2/internal/plan" xprender "github.com/crossplane/cli/v2/pkg/render" ) @@ -137,8 +138,7 @@ func TestSimulateScope(t *testing.T) { func TestSimulateProcessorOptions(t *testing.T) { stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{} k := &kong.Context{Kong: &kong.Kong{Stdout: stdout, Stderr: stderr}} - c := &Flags{ - sel: CompositionFlags{IncludePinned: true, AnalyzeOn: "spec-change"}, + c := &runner{sel: CompositionFlags{IncludePinned: true, AnalyzeOn: "spec-change"}, Flags: &Flags{ MaxIterations: 7, EngineFlags: xprender.EngineFlags{CrossplaneVersion: "v2.3.4", CrossplaneDockerNetwork: "ci"}, @@ -147,7 +147,7 @@ func TestSimulateProcessorOptions(t *testing.T) { EventualState: true, MaxRecvMessageSize: 16, DryRunOn: "existing", - } + }} creds := []corev1.Secret{{ObjectMeta: metav1.ObjectMeta{Name: "creds"}}} cfg := dp.ProcessorConfig{} @@ -329,7 +329,7 @@ func TestSimulateDiffOptions(t *testing.T) { want := renderer.DefaultDiffOptions() tc.want(&want) - got := tc.cmd.diffOptions(k) + got := (&runner{Flags: &tc.cmd}).diffOptions(k) // Writers are interfaces holding the kong context's streams; compare them by identity. if got.Stdout != stdout || got.Stderr != stderr { t.Errorf("\n%s\ndiffOptions(...) writers: got (%v, %v), want the kong context's", tc.reason, got.Stdout, got.Stderr) @@ -425,11 +425,11 @@ func TestSimulateRenderReports(t *testing.T) { want func(*testing.T, string) }{ "xr": {func(k *kong.Context) error { - _, err := tc.cmd.renderXRReport(k, logging.NewNopLogger(), xrReport, nil) + _, err := (&runner{Flags: &tc.cmd}).renderXRReport(k, logging.NewNopLogger(), xrReport, nil) return err }, tc.wantXR}, "comp": {func(k *kong.Context) error { - _, err := tc.cmd.renderCompReport(k, logging.NewNopLogger(), compReport, nil) + _, err := (&runner{Flags: &tc.cmd}).renderCompReport(k, logging.NewNopLogger(), compReport, nil) return err }, tc.wantComp}, } { @@ -573,7 +573,7 @@ func TestSimulateRun(t *testing.T) { } type args struct { - cmd Flags + cmd runner initClients func(context.Context, logging.Logger) error proc *fakeCompProc cancel bool @@ -622,19 +622,19 @@ func TestSimulateRun(t *testing.T) { }, "RenderErrorKeepsDiffError": { reason: "A render failure should be joined with the simulation's error, not replace it.", - args: args{cmd: Flags{Output: "xml"}, initClients: noInit, proc: &fakeCompProc{diffFn: func(context.Context) (*dt.CompDiffOutput, error) { + args: args{cmd: runner{Flags: &Flags{Output: "xml"}}, initClients: noInit, proc: &fakeCompProc{diffFn: func(context.Context) (*dt.CompDiffOutput, error) { return changed(), errBoom }}}, want: want{code: dp.ExitCodeToolError, err: errBoom, errMsg: "failed to render diffs", diffed: true}, }, "Namespaced": { reason: "The namespace Validate scoped to should reach DiffComposition.", - args: args{cmd: Flags{sel: CompositionFlags{ns: "team-a"}}, initClients: noInit, proc: &fakeCompProc{}}, + args: args{cmd: runner{sel: CompositionFlags{ns: "team-a"}}, initClients: noInit, proc: &fakeCompProc{}}, want: want{diffed: true, namespace: "team-a"}, }, "Scoped": { reason: "The refs Validate scoped to should reach DiffComposition.", - args: args{cmd: Flags{sel: CompositionFlags{refs: []k8stypes.NamespacedName{{Namespace: "team-a", Name: "db"}}}}, initClients: noInit, proc: &fakeCompProc{}}, + args: args{cmd: runner{sel: CompositionFlags{refs: []k8stypes.NamespacedName{{Namespace: "team-a", Name: "db"}}}}, initClients: noInit, proc: &fakeCompProc{}}, want: want{diffed: true, refs: []k8stypes.NamespacedName{{Namespace: "team-a", Name: "db"}}}, }, "ClientInitError": { @@ -696,6 +696,9 @@ func TestSimulateRun(t *testing.T) { } // kong sets -o's default when it parses the flags. + if tc.args.cmd.Flags == nil { + tc.args.cmd.Flags = &Flags{} + } if tc.args.cmd.Output == "" { tc.args.cmd.Output = "plan" } @@ -711,7 +714,7 @@ func TestSimulateRun(t *testing.T) { t.Errorf("\n%s\nsimulate(...): -want exit code, +got:\n%s", tc.reason, diff) } // Changes found return a silent ExitError, which main prints nothing for. - if _, silent := SilentExit(err); tc.want.err == nil && err != nil && !silent { + if _, silent := exitcode.SilentExit(err); tc.want.err == nil && err != nil && !silent { t.Errorf("\n%s\nsimulate(...): want no error or a silent exit, got %v", tc.reason, err) } if tc.want.err != nil && !errors.Is(err, tc.want.err) { @@ -840,10 +843,10 @@ func TestPlanNotes(t *testing.T) { // TestExitCodesMatchEngine pins the CLI's exit statuses to the engine's, so // DetermineExitCode's ranking keeps meaning what the help says. func TestExitCodesMatchEngine(t *testing.T) { - if diff := cmp.Diff(dp.ExitCodeDiffDetected, ExitCodeChanges); diff != "" { - t.Errorf("ExitCodeChanges: -engine, +cli:\n%s", diff) + if diff := cmp.Diff(dp.ExitCodeDiffDetected, exitcode.ExitCodeChanges); diff != "" { + t.Errorf("exitcode.ExitCodeChanges: -engine, +cli:\n%s", diff) } - if diff := cmp.Diff(dp.ExitCodeSchemaValidation, ExitCodeSchemaValidation); diff != "" { + if diff := cmp.Diff(dp.ExitCodeSchemaValidation, exitcode.ExitCodeSchemaValidation); diff != "" { t.Errorf("ExitCodeSchemaValidation: -engine, +cli:\n%s", diff) } } @@ -864,7 +867,7 @@ func TestNoteNoServers(t *testing.T) { t.Run(name, func(t *testing.T) { stderr := &bytes.Buffer{} k := &kong.Context{Kong: &kong.Kong{Stderr: stderr}} - c := &Flags{Output: tc.output} + c := &runner{Flags: &Flags{Output: tc.output}} c.noteNoServers(k, tc.notes) c.noteNoServers(k, tc.notes) if diff := cmp.Diff(tc.want, stderr.String()); diff != "" { @@ -936,7 +939,7 @@ func TestSimulateProcessorOptionsDockerNetwork(t *testing.T) { } k := &kong.Context{Kong: &kong.Kong{Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}}} cfg := dp.ProcessorConfig{} - for _, o := range c.processorOptions(k, logging.NewNopLogger(), nil, fns, nil, nil) { + for _, o := range (&runner{Flags: c}).processorOptions(k, logging.NewNopLogger(), nil, fns, nil, nil) { o(&cfg) } if cfg.CrossplaneDockerNetwork != tc.want { @@ -1038,7 +1041,7 @@ func TestDiffComposites(t *testing.T) { } return &dt.XRReport{Groups: []dt.XRDiffGroup{g}}, tc.diffErr }} - c := &Flags{Output: "plan", NoColor: true} + c := &runner{Flags: &Flags{Output: "plan", NoColor: true}} stdout := &bytes.Buffer{} k := &kong.Context{Kong: &kong.Kong{Stdout: stdout, Stderr: &bytes.Buffer{}}} xp := &xpclient.Clients{Definition: defs, Composition: &testutils.MockCompositionClient{}} diff --git a/internal/testutils/integration/simharness/harness.go b/internal/testutils/integration/simharness/harness.go index 2b54d56d..992a1d43 100644 --- a/internal/testutils/integration/simharness/harness.go +++ b/internal/testutils/integration/simharness/harness.go @@ -62,6 +62,7 @@ import ( "github.com/crossplane/cli/v2/internal/diff/ref" tu "github.com/crossplane/cli/v2/internal/diff/testutils" "github.com/crossplane/cli/v2/internal/docker" + "github.com/crossplane/cli/v2/internal/exitcode" "github.com/crossplane/cli/v2/internal/simulate" "github.com/crossplane/cli/v2/internal/terminal" clixpkg "github.com/crossplane/cli/v2/internal/xpkg" @@ -261,7 +262,7 @@ func RunSimulateArgvWith(t *testing.T, cfg *rest.Config, argv []string, hooks *s // Exit as main does: a silent ExitError, such as changes found, exits // with its code and prints nothing; any other error is printed the way // FatalIfErrorf does and exits with its ExitCoder's code or 1. - if code, ok := simulate.SilentExit(err); ok { + if code, ok := exitcode.SilentExit(err); ok { return code, nil } if err != nil {