diff --git a/client/building_block_v2.go b/client/building_block_v2.go index 01d204d..8f81fcc 100644 --- a/client/building_block_v2.go +++ b/client/building_block_v2.go @@ -266,50 +266,11 @@ func (status MeshBuildingBlockV2Status) RunFailed() bool { return slices.Contains(buildingBlockActionsRunFailed, status.Action) } -// RunChangedSince says whether meshStack has reacted to a write since before, by the rules of -// https://docs.meshcloud.io/concepts/building-block/#waiting-for-a-run. A zero before is a block that had no status yet. -func (status MeshBuildingBlockV2Status) RunChangedSince(before MeshBuildingBlockV2Status) bool { - switch { - case status.GetLatestRunUuid() != before.GetLatestRunUuid(), - status.GetLatestDryRunUuid() != before.GetLatestDryRunUuid(), - status.RunStartFailedOn != "" && status.RunStartFailedOn != before.RunStartFailedOn: - return true - case before.RunStartFailedOn != "" && status.RunStartFailedOn == "": - // A build that includes the block again only clears the old failure. - return false - default: - return !before.IsRunning() && status.actionOfRun() != before.actionOfRun() - } -} - -// actionOfRun ignores UPGRADE_AVAILABLE, which a newly released definition version sets without any run. -func (status MeshBuildingBlockV2Status) actionOfRun() enum.Entry[BuildingBlockAction] { - if status.Action == BuildingBlockActionUpgradeAvailable { - return BuildingBlockActionNoAction - } - return status.Action -} - func (bb *MeshBuildingBlockV2) IsWaitingForInput() bool { return bb.Status != nil && bb.Status.IsWaitingForInput() } -// RunEndedSince is done on every action that is not running, a failed or an unknown one included: -// the caller judges the outcome. -func (bb *MeshBuildingBlockV2) RunEndedSince(before MeshBuildingBlockV2Status) (done bool, err error) { - switch { - case bb == nil: - err = errors.New("building block not found") - case bb.Status == nil, !bb.Status.RunChangedSince(before), bb.Status.IsRunning(): - default: - done = true - } - return -} - -// DeletionSuccessful takes the status the block had before the deletion, because meshStack reports the -// action of the run before until the delete run starts. -func (bb *MeshBuildingBlockV2) DeletionSuccessful(before MeshBuildingBlockV2Status) (done bool, err error) { +func (bb *MeshBuildingBlockV2) DeletionSuccessful() (done bool, err error) { switch { case bb == nil: // A 404: the block is gone, for example together with its definition. @@ -318,7 +279,7 @@ func (bb *MeshBuildingBlockV2) DeletionSuccessful(before MeshBuildingBlockV2Stat // A soft-deleted block does not return 404: the backend keeps returning it with DELETED. // MARKED_FOR_DELETION means the deletion still runs, so polling goes on. done = true - case bb.Status == nil, !bb.Status.RunChangedSince(before): + case bb.Status == nil, bb.Status.IsRunning(): case bb.Status.ForcePurge: // A force purge (definition deletion_mode PURGE, or an admin purge) deletes the block whatever // its delete run reports, and the lifecycle still reaches DELETED. @@ -345,9 +306,8 @@ func (bb *MeshBuildingBlockV2) uuidOrUnknown() string { return "" } -// TriggerRun answers with the building block as meshStack left it on accepting the run: Action, -// LatestRunUuid, RunStartFailure and RunStartFailedOn still report the request before, because -// meshStack starts the run, or records why it could not, only after it answered. +// TriggerRun answers with the building block in the action PENDING, as meshStack starts the run, or +// records why it could not, only after it answered. func (c meshBuildingBlockV2Client) TriggerRun(ctx context.Context, bbUuid string) (*MeshBuildingBlockV2, error) { // dryRun is not optional to the endpoint once a body is sent, so it goes out as false rather // than being omitted. diff --git a/client/building_block_v2_test.go b/client/building_block_v2_test.go index 9666322..4959d33 100644 --- a/client/building_block_v2_test.go +++ b/client/building_block_v2_test.go @@ -9,108 +9,7 @@ import ( "github.com/meshcloud/meshstack-cli/client/types/enum" ) -func TestMeshBuildingBlockV2Status_RunChangedSince(t *testing.T) { - const ( - failedOn = "2026-10-28T12:00:00Z" - laterFailedOn = "2026-10-28T13:00:00Z" - ) - idle := MeshBuildingBlockV2Status{Action: BuildingBlockActionNoAction, LatestRunUuid: new("run-1")} - with := func(base MeshBuildingBlockV2Status, change func(*MeshBuildingBlockV2Status)) MeshBuildingBlockV2Status { - change(&base) - return base - } - tests := []struct { - name string - before MeshBuildingBlockV2Status - now MeshBuildingBlockV2Status - want bool - }{ - {"a block without a status before has changed", MeshBuildingBlockV2Status{}, idle, true}, - {"a created block that waits for its first run has changed against a zero before", MeshBuildingBlockV2Status{}, MeshBuildingBlockV2Status{Action: BuildingBlockActionPending}, true}, - {"the same status has not changed", idle, idle, false}, - {"a new latest run has changed", idle, with(idle, func(s *MeshBuildingBlockV2Status) { s.LatestRunUuid = new("run-2") }), true}, - {"a new latest dry run has changed", idle, with(idle, func(s *MeshBuildingBlockV2Status) { s.LatestDryRunUuid = new("dry-run-1") }), true}, - {"a first run start failure has changed", idle, with(idle, func(s *MeshBuildingBlockV2Status) { - s.Action, s.RunStartFailedOn = BuildingBlockActionReviewErrors, failedOn - }), true}, - { - "a newer run start failure has changed", - with(idle, func(s *MeshBuildingBlockV2Status) { s.RunStartFailedOn = failedOn }), - with(idle, func(s *MeshBuildingBlockV2Status) { s.RunStartFailedOn = laterFailedOn }), true, - }, - { - "a cleared run start failure alone has not changed", - with(idle, func(s *MeshBuildingBlockV2Status) { - s.Action, s.RunStartFailedOn = BuildingBlockActionReviewErrors, failedOn - }), - idle, false, - }, - { - "a cleared run start failure with a new run has changed", - with(idle, func(s *MeshBuildingBlockV2Status) { - s.Action, s.RunStartFailedOn = BuildingBlockActionReviewErrors, failedOn - }), - with(idle, func(s *MeshBuildingBlockV2Status) { - s.Action, s.LatestRunUuid = BuildingBlockActionPending, new("run-2") - }), true, - }, - { - "a new action of a block that was not running has changed, as supplied input resumes the same run", - with(idle, func(s *MeshBuildingBlockV2Status) { s.Action = BuildingBlockActionProvideInput }), - with(idle, func(s *MeshBuildingBlockV2Status) { s.Action = BuildingBlockActionPending }), true, - }, - { - "a new action of a block that was running has not changed, as it belongs to the run before", - with(idle, func(s *MeshBuildingBlockV2Status) { s.Action = BuildingBlockActionInProgress }), - with(idle, func(s *MeshBuildingBlockV2Status) { s.Action = BuildingBlockActionReviewErrors }), false, - }, - { - "UPGRADE_AVAILABLE reads as NO_ACTION, as a released definition version starts no run", - idle, with(idle, func(s *MeshBuildingBlockV2Status) { s.Action = BuildingBlockActionUpgradeAvailable }), false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.now.RunChangedSince(tt.before)) - }) - } -} - -func TestMeshBuildingBlockV2_RunEndedSince(t *testing.T) { - before := MeshBuildingBlockV2Status{Action: BuildingBlockActionReviewErrors, LatestRunUuid: new("run-1")} - block := func(action enum.Entry[BuildingBlockAction], latestRunUuid string) *MeshBuildingBlockV2 { - return &MeshBuildingBlockV2{Status: &MeshBuildingBlockV2Status{Action: action, LatestRunUuid: new(latestRunUuid)}} - } - tests := []struct { - name string - bb *MeshBuildingBlockV2 - wantDone bool - }{ - {"a block without a status goes on", &MeshBuildingBlockV2{}, false}, - {"the outcome of the run before goes on", block(BuildingBlockActionReviewErrors, "run-1"), false}, - {"a pending run goes on", block(BuildingBlockActionPending, "run-2"), false}, - {"a run in progress goes on", block(BuildingBlockActionInProgress, "run-2"), false}, - {"a run that needs no action ends", block(BuildingBlockActionNoAction, "run-2"), true}, - {"a failed run ends without an error", block(BuildingBlockActionReviewErrors, "run-2"), true}, - {"a run that waits for approval ends", block(BuildingBlockActionApproveRun, "run-2"), true}, - {"an action this client does not know ends without an error", block(enum.Entry[BuildingBlockAction]("SOMETHING_NEW"), "run-2"), true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - done, err := tt.bb.RunEndedSince(before) - assert.Equal(t, tt.wantDone, done) - assert.NoError(t, err) - }) - } - - t.Run("a block that is gone fails", func(t *testing.T) { - _, err := (*MeshBuildingBlockV2)(nil).RunEndedSince(MeshBuildingBlockV2Status{}) - require.Error(t, err) - }) -} - func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { - before := MeshBuildingBlockV2Status{Action: BuildingBlockActionReviewErrors, LatestRunUuid: new("apply-run")} deleteRun := func(action enum.Entry[BuildingBlockAction]) *MeshBuildingBlockV2 { return &MeshBuildingBlockV2{ Metadata: MeshBuildingBlockV2Metadata{Uuid: new("test-uuid")}, @@ -129,10 +28,7 @@ func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { bb: &MeshBuildingBlockV2{Status: &MeshBuildingBlockV2Status{Lifecycle: MeshBuildingBlockV2Lifecycle{State: BuildingBlockLifecycleStateDeleted}}}, wantDone: true, }, - { - name: "REVIEW_ERRORS of the run before the deletion goes on", - bb: &MeshBuildingBlockV2{Status: &MeshBuildingBlockV2Status{Action: BuildingBlockActionReviewErrors, LatestRunUuid: new("apply-run")}}, - }, + {name: "a delete run that meshStack has not started yet goes on", bb: deleteRun(BuildingBlockActionPending)}, {name: "a delete run in progress goes on", bb: deleteRun(BuildingBlockActionInProgress)}, {name: "a failed delete run fails", bb: deleteRun(BuildingBlockActionReviewErrors), wantErr: "the delete run of building block test-uuid ended with action REVIEW_ERRORS"}, {name: "an aborted delete run fails", bb: deleteRun(BuildingBlockActionReviewAbortedRun), wantErr: "ended with action REVIEW_ABORTED_RUN"}, @@ -159,7 +55,7 @@ func TestMeshBuildingBlockV2_DeletionSuccessful(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - done, err := tt.bb.DeletionSuccessful(before) + done, err := tt.bb.DeletionSuccessful() assert.Equal(t, tt.wantDone, done) if tt.wantErr == "" { assert.NoError(t, err) diff --git a/cmd/buildingblock/trigger_run.go b/cmd/buildingblock/trigger_run.go index bf48d67..abcec65 100644 --- a/cmd/buildingblock/trigger_run.go +++ b/cmd/buildingblock/trigger_run.go @@ -26,7 +26,7 @@ func newTriggerRun() *cobra.Command { The command waits until meshStack has started the run, and then prints the building block. Its status.latestRunUuid is the run. If the definition requires an approval of the run, status.latestDryRunUuid is the preflight run, whose plan the approver sees. 'meshstack buildingblockrun logs ' shows how far a run got. -With --` + waitFlagName + `, the command waits until the run has ended. This is the same as piping meshStack's response to the run request into 'meshstack buildingblock wait --` + beforeFlagName + `=-'. +With --` + waitFlagName + `, the command waits until the run has ended. This is the same as running 'meshstack buildingblock wait ' afterwards. 'meshstack api-docs --describe buildingblock.trigger-run' shows the fields of the response.`, Example: ` meshstack buildingblock trigger-run 0b5c1d3e-5f1a-4c2b-9d7e-2a6f8e4b1c90 @@ -47,14 +47,14 @@ With --` + waitFlagName + `, the command waits until the run has ended. This is accepted = *answer.Status } if wait { - return awaitRunEnd(ctx, cmd.OutOrStdout(), output.Format, meshStack.Raw, buildingBlockUuid, accepted) + return awaitRunEnd(ctx, cmd.OutOrStdout(), output.Format, meshStack.Raw, buildingBlockUuid) } runWait := buildingblock.NewRunWait(meshStack.Raw, buildingBlockUuid) - reacted, block, err := runWait.AwaitReaction(ctx, accepted) + started, block, err := runWait.AwaitStart(ctx) if err != nil { return err } - if err := output.Format.WriteItem(cmd.OutOrStdout(), reacted); err != nil { + if err := output.Format.WriteItem(cmd.OutOrStdout(), started); err != nil { return err } if err := runWait.StartFailure(block); err != nil { @@ -73,8 +73,8 @@ With --` + waitFlagName + `, the command waits until the run has ended. This is const waitFlagName = "wait" -func startedRunMessage(buildingBlockUuid uuid.UUID, accepted, reacted client.MeshBuildingBlockV2Status) string { - switch dryRun, run := reacted.GetLatestDryRunUuid(), reacted.GetLatestRunUuid(); { +func startedRunMessage(buildingBlockUuid uuid.UUID, accepted, started client.MeshBuildingBlockV2Status) string { + switch dryRun, run := started.GetLatestDryRunUuid(), started.GetLatestRunUuid(); { case dryRun != "" && dryRun != accepted.GetLatestDryRunUuid(): return fmt.Sprintf("meshStack started the preflight run %s of building block %s. The run that makes the changes then waits for approval. "+ "Follow the preflight run with 'meshstack buildingblockrun logs %s'", dryRun, buildingBlockUuid, dryRun) @@ -82,6 +82,6 @@ func startedRunMessage(buildingBlockUuid uuid.UUID, accepted, reacted client.Mes return fmt.Sprintf("meshStack started run %s of building block %s. Follow it with 'meshstack buildingblockrun logs %s'", run, buildingBlockUuid, run) default: - return fmt.Sprintf("Building block %s now has the action %s.", buildingBlockUuid, reacted.Action) + return fmt.Sprintf("Building block %s now has the action %s.", buildingBlockUuid, started.Action) } } diff --git a/cmd/buildingblock/trigger_run_test.go b/cmd/buildingblock/trigger_run_test.go index 90ea457..94da1d4 100644 --- a/cmd/buildingblock/trigger_run_test.go +++ b/cmd/buildingblock/trigger_run_test.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json/jsontext" "fmt" - "io" gohttp "net/http" "testing" @@ -23,15 +22,13 @@ func TestTriggerRunPrintsTheBuildingBlockAndFailsWithTheReasonMeshStackCouldNotS buildingBlockUuid = "b1d2c3e4-0000-4000-8000-000000000001" reason = "meshStack could not start a run for this Building Block.\n\nAsk your platform team to look up the details in the meshStack logs." ) - failure := func(failedOn string) string { - return fmt.Sprintf(`{"metadata": {"uuid": %q, "ownedByWorkspace": "my-workspace"}, "spec": {}, `+ - `"status": {"action": "PENDING", "latestRunUuid": "a0000000-0000-4000-8000-000000000001", "runStartFailure": %q, "runStartFailedOn": %q}}`, - buildingBlockUuid, reason, failedOn) - } - meshStack := fakemeshstack.Start(t, fakemeshstack.Options{BuildingBlocks: []any{jsontext.Value(failure("2026-10-01T10:05:00Z"))}}) + failure := fmt.Sprintf(`{"metadata": {"uuid": %q, "ownedByWorkspace": "my-workspace"}, "spec": {}, `+ + `"status": {"action": "REVIEW_ERRORS", "latestRunUuid": "a0000000-0000-4000-8000-000000000001", "runStartFailure": %q, "runStartFailedOn": "2026-10-01T10:05:00Z"}}`, + buildingBlockUuid, reason) + meshStack := fakemeshstack.Start(t, fakemeshstack.Options{BuildingBlocks: []any{jsontext.Value(failure)}}) meshStack.Route("POST /api/meshobjects/meshbuildingblocks/{uuid}/trigger-run", func(w gohttp.ResponseWriter, _ *gohttp.Request) { w.WriteHeader(gohttp.StatusAccepted) - _, _ = io.WriteString(w, failure("2026-10-01T10:00:00Z")) + _, _ = fmt.Fprintf(w, `{"metadata": {"uuid": %q}, "spec": {}, "status": {"action": "PENDING", "latestRunUuid": "a0000000-0000-4000-8000-000000000001"}}`, buildingBlockUuid) }) testlogin.LoggedInTo(t, meshStack.URL) diff --git a/cmd/buildingblock/wait.go b/cmd/buildingblock/wait.go index c420e28..695d568 100644 --- a/cmd/buildingblock/wait.go +++ b/cmd/buildingblock/wait.go @@ -18,62 +18,38 @@ func newWait() *cobra.Command { var ( output internal.OutputFlag buildingBlockUuid uuid.UUID - beforeInput internal.JsonInputFlag[client.MeshBuildingBlockV2] ) cmd := &cobra.Command{ - Use: "wait --before ", + Use: "wait ", Short: "Wait until the run of a building block has ended", - Long: `Wait until meshStack has reacted to a write of a building block, and until the run that the write started has ended. Give the building block by its uuid. + Long: `Wait until the run of a building block has ended. Give the building block by its uuid. -meshStack responds to a write before it starts the run. So right after the write, the building block still shows the action of the previous run. Pass that response with --` + beforeFlagName + `: the response of a create (POST), an update (PUT) or a trigger-run, or a read taken before the change. The command first waits up to a minute until meshStack has reacted, following the rules of https://docs.meshcloud.io/concepts/building-block/#waiting-for-a-run. Then it waits until the run has ended. Press Ctrl-C to stop waiting. +After a write that asks for a run, such as a create (POST), an update (PUT) or a trigger-run, meshStack shows the action PENDING until it has started the run, and IN_PROGRESS while the run is going on. The command waits while the action is PENDING or IN_PROGRESS. Press Ctrl-C to stop waiting. The command prints the building block. It fails and names the action if the run failed, was aborted or could not start, or if the run is waiting for a person or a dependency.`, - Example: ` meshstack api -X PUT /api/meshobjects/meshbuildingblocks/0b5c1d3e-5f1a-4c2b-9d7e-2a6f8e4b1c90 --request-json block.json | - meshstack bb wait 0b5c1d3e-5f1a-4c2b-9d7e-2a6f8e4b1c90 --before=-`, + Example: ` meshstack api -X PUT /api/meshobjects/meshbuildingblocks/0b5c1d3e-5f1a-4c2b-9d7e-2a6f8e4b1c90 --request-json block.json && + meshstack bb wait 0b5c1d3e-5f1a-4c2b-9d7e-2a6f8e4b1c90`, Args: internal.UuidArg(&buildingBlockUuid), RunE: func(cmd *cobra.Command, _ []string) error { - before, err := readBefore(cmd, beforeInput, buildingBlockUuid) - if err != nil { - return err - } meshStack, err := internal.ResolveClient(cmd.Context()) if err != nil { return err } - return awaitRunEnd(cmd.Context(), cmd.OutOrStdout(), output.Format, meshStack.Raw, buildingBlockUuid, before) + return awaitRunEnd(cmd.Context(), cmd.OutOrStdout(), output.Format, meshStack.Raw, buildingBlockUuid) }, } output.RegisterForItem(cmd.Flags()) - beforeInput.Register(cmd.Flags(), beforeFlagName, "file with the building block from before the write, or - to read it from stdin") - _ = cmd.MarkFlagRequired(beforeFlagName) return cmd } -const beforeFlagName = "before" - -func readBefore(cmd *cobra.Command, input internal.JsonInputFlag[client.MeshBuildingBlockV2], buildingBlockUuid uuid.UUID) (client.MeshBuildingBlockV2Status, error) { - source := input.Source - before, err := input.Read(cmd) - if err != nil { - return client.MeshBuildingBlockV2Status{}, err - } - switch { - case before.Metadata.Uuid != nil && *before.Metadata.Uuid != buildingBlockUuid.String(): - return client.MeshBuildingBlockV2Status{}, fmt.Errorf("--%s %s contains building block %s, but the command waits for %s", beforeFlagName, source, *before.Metadata.Uuid, buildingBlockUuid) - case before.Status == nil: - return client.MeshBuildingBlockV2Status{}, fmt.Errorf("--%s %s contains no status of the building block", beforeFlagName, source) - } - return *before.Status, nil -} - // awaitRunEnd writes the building block once its run has ended, and fails on an outcome someone has // to act on. -func awaitRunEnd(ctx context.Context, out io.Writer, format internal.OutputFormat, raw *client.RawClient, buildingBlockUuid uuid.UUID, before client.MeshBuildingBlockV2Status) error { +func awaitRunEnd(ctx context.Context, out io.Writer, format internal.OutputFormat, raw *client.RawClient, buildingBlockUuid uuid.UUID) error { wait := buildingblock.NewRunWait(raw, buildingBlockUuid) - ended, block, err := wait.AwaitEnd(ctx, before) + ended, block, err := wait.AwaitEnd(ctx) if err != nil { return err } diff --git a/cmd/buildingblock/wait_test.go b/cmd/buildingblock/wait_test.go index 2202cb4..905089e 100644 --- a/cmd/buildingblock/wait_test.go +++ b/cmd/buildingblock/wait_test.go @@ -5,7 +5,6 @@ import ( "fmt" "io" gohttp "net/http" - "strings" "sync/atomic" "testing" @@ -26,7 +25,6 @@ func buildingBlockWithRun(action, latestRunUuid string) string { } func TestWaitFailsOnAnOutcomeThatNeedsSomeoneToAct(t *testing.T) { - before := buildingBlockWithRun("NO_ACTION", "run-1") var current atomic.Pointer[string] meshStack := fakemeshstack.Start(t, fakemeshstack.Options{}) meshStack.Route("GET /api/meshobjects/meshbuildingblocks/{uuid}", func(w gohttp.ResponseWriter, _ *gohttp.Request) { @@ -34,24 +32,18 @@ func TestWaitFailsOnAnOutcomeThatNeedsSomeoneToAct(t *testing.T) { }) testlogin.LoggedInTo(t, meshStack.URL) - run := func(stdin string, args ...string) (string, error) { + run := func(args ...string) (string, error) { cmd := buildingblock.New() var stdout bytes.Buffer cmd.SetOut(&stdout) - cmd.SetIn(strings.NewReader(stdin)) cmd.SetArgs(args) err := cmd.ExecuteContext(t.Context()) return stdout.String(), err } - t.Run("wait needs the building block from before the write", func(t *testing.T) { - _, err := run("", "wait", waitedBuildingBlock) - require.ErrorContains(t, err, `required flag(s) "before" not set`) - }) - t.Run("a failed run fails the wait with its action and the meshPanel link", func(t *testing.T) { current.Store(new(buildingBlockWithRun("REVIEW_ERRORS", "run-2"))) - stdout, err := run(before, "wait", waitedBuildingBlock, "--before=-") + stdout, err := run("wait", waitedBuildingBlock) require.ErrorContains(t, err, "ended with action REVIEW_ERRORS") require.ErrorContains(t, err, "https://panel.example/#/go/buildingblock/"+waitedBuildingBlock) assert.Contains(t, stdout, `"action": "REVIEW_ERRORS"`, "the building block still goes to stdout") @@ -59,7 +51,7 @@ func TestWaitFailsOnAnOutcomeThatNeedsSomeoneToAct(t *testing.T) { t.Run("a run that waits for approval fails the wait", func(t *testing.T) { current.Store(new(buildingBlockWithRun("APPROVE_RUN", "run-2"))) - _, err := run(before, "wait", waitedBuildingBlock, "--before=-") + _, err := run("wait", waitedBuildingBlock) require.ErrorContains(t, err, "is paused at action APPROVE_RUN") }) } diff --git a/cmd/internal/buildingblock/run_wait.go b/cmd/internal/buildingblock/run_wait.go index 57ec778..abc9326 100644 --- a/cmd/internal/buildingblock/run_wait.go +++ b/cmd/internal/buildingblock/run_wait.go @@ -1,5 +1,4 @@ -// Package buildingblock waits for the run of a building block, by the rules of -// [client.MeshBuildingBlockV2Status.RunChangedSince] and [client.MeshBuildingBlockV2.RunEndedSince]. +// Package buildingblock waits for the run of a building block. package buildingblock import ( @@ -14,46 +13,32 @@ import ( ) const ( - // runStartTimeout is runStartTimeout of terraform-provider-meshstack, which waits for the same: - // starting a run is a write of meshStack's own, so a meshStack that has not done it within a - // minute is not going to. - runStartTimeout = time.Minute - runStartPollInterval = time.Second - runEndPollInterval = 10 * time.Second + pendingPollInterval = time.Second + inProgressPollInterval = 10 * time.Second ) type RunWait struct { raw *client.RawClient buildingBlockUuid uuid.UUID - startEvery time.Duration - startWithin time.Duration - endEvery time.Duration + pendingEvery time.Duration + inProgressEvery time.Duration } func NewRunWait(raw *client.RawClient, buildingBlockUuid uuid.UUID) RunWait { - return RunWait{raw, buildingBlockUuid, runStartPollInterval, runStartTimeout, runEndPollInterval} + return RunWait{raw, buildingBlockUuid, pendingPollInterval, inProgressPollInterval} } -// AwaitReaction returns the building block once meshStack has reacted to a write since before. -func (w RunWait) AwaitReaction(ctx context.Context, before client.MeshBuildingBlockV2Status) (jsontext.Value, *client.MeshBuildingBlockV2, error) { - ctx, cancel := context.WithTimeoutCause(ctx, w.startWithin, fmt.Errorf( - "meshStack did not react to the write of building block %s within %s: it started no run and reported no failure to start one", - w.buildingBlockUuid, w.startWithin)) - defer cancel() - reacted, block, err := w.poll(ctx, w.startEvery, func(block *client.MeshBuildingBlockV2) (bool, error) { - return block.Status != nil && block.Status.RunChangedSince(before), nil +// AwaitStart returns the building block once meshStack has started the run, or decided that none starts. +func (w RunWait) AwaitStart(ctx context.Context) (jsontext.Value, *client.MeshBuildingBlockV2, error) { + started, block, err := w.poll(ctx, func(status client.MeshBuildingBlockV2Status) bool { + return status.Action != client.BuildingBlockActionPending }) - return reacted, block, withMeshPanelLink(err, block) + return started, block, withMeshPanelLink(err, block) } -// AwaitEnd returns the building block once meshStack has reacted to a write since before, and the run -// is no longer running. -func (w RunWait) AwaitEnd(ctx context.Context, before client.MeshBuildingBlockV2Status) (jsontext.Value, *client.MeshBuildingBlockV2, error) { - if _, _, err := w.AwaitReaction(ctx, before); err != nil { - return nil, nil, err - } - ended, block, err := w.poll(ctx, w.endEvery, func(block *client.MeshBuildingBlockV2) (bool, error) { - return block.RunEndedSince(client.MeshBuildingBlockV2Status{}) +func (w RunWait) AwaitEnd(ctx context.Context) (jsontext.Value, *client.MeshBuildingBlockV2, error) { + ended, block, err := w.poll(ctx, func(status client.MeshBuildingBlockV2Status) bool { + return !status.IsRunning() }) return ended, block, withMeshPanelLink(err, block) } @@ -87,9 +72,8 @@ func withMeshPanelLink(err error, block *client.MeshBuildingBlockV2) error { return err } -// poll reads the building block every interval until done says so, and returns the last read, also -// when ctx ends the poll. -func (w RunWait) poll(ctx context.Context, every time.Duration, done func(*client.MeshBuildingBlockV2) (bool, error)) (jsontext.Value, *client.MeshBuildingBlockV2, error) { +// poll returns the last read also when ctx ends the poll. +func (w RunWait) poll(ctx context.Context, done func(client.MeshBuildingBlockV2Status) bool) (jsontext.Value, *client.MeshBuildingBlockV2, error) { var last *client.MeshBuildingBlockV2 for { current, err := w.raw.Get[client.MeshBuildingBlockV2](ctx, w.buildingBlockUuid) @@ -103,8 +87,14 @@ func (w RunWait) poll(ctx context.Context, every time.Duration, done func(*clien return nil, last, err } last = &block - if ended, err := done(last); ended || err != nil { - return current, last, err + every := w.inProgressEvery + if block.Status != nil { + if done(*block.Status) { + return current, last, nil + } + if block.Status.Action == client.BuildingBlockActionPending { + every = w.pendingEvery + } } select { case <-ctx.Done(): diff --git a/cmd/internal/buildingblock/run_wait_test.go b/cmd/internal/buildingblock/run_wait_test.go index 8f5109a..e2718a3 100644 --- a/cmd/internal/buildingblock/run_wait_test.go +++ b/cmd/internal/buildingblock/run_wait_test.go @@ -19,40 +19,48 @@ import ( func TestRunWait(t *testing.T) { buildingBlockUuid := uuid.MustParse("b1d2c3e4-0000-4000-8000-000000000003") - before := client.MeshBuildingBlockV2Status{Action: client.BuildingBlockActionNoAction, LatestRunUuid: new("run-1")} - var status atomic.Pointer[string] + var statuses atomic.Pointer[[]string] meshStack := fakemeshstack.Start(t, fakemeshstack.Options{}) meshStack.Route("GET /api/meshobjects/meshbuildingblocks/{uuid}", func(w gohttp.ResponseWriter, _ *gohttp.Request) { + remaining := *statuses.Load() + if len(remaining) > 1 { + statuses.Store(new(remaining[1:])) + } _, _ = fmt.Fprintf(w, `{"metadata": {"uuid": %q}, "spec": {}, "status": {%s}, "_links": {"meshPanel": {"href": "https://panel.example/block"}}}`, - buildingBlockUuid, *status.Load()) + buildingBlockUuid, remaining[0]) }) raw := client.New(t.Context(), xurl.MustParsef("%s", meshStack.URL), http.NewClient(fakemeshstack.UserAgent), http.BearerToken(fakemeshstack.Token)).Raw - wait := RunWait{raw: raw, buildingBlockUuid: buildingBlockUuid, startEvery: time.Millisecond, startWithin: 50 * time.Millisecond} + wait := RunWait{raw: raw, buildingBlockUuid: buildingBlockUuid, pendingEvery: time.Millisecond, inProgressEvery: time.Millisecond} + const ( + pending = `"action": "PENDING", "latestRunUuid": "run-1"` + inProgress = `"action": "IN_PROGRESS", "latestRunUuid": "run-2"` + ) - t.Run("a preflight run that a trigger with an approval policy starts is a reaction", func(t *testing.T) { - status.Store(new(`"action": "IN_PROGRESS", "latestRunUuid": "run-1", "latestDryRunUuid": "preflight-1"`)) + t.Run("the start ends with the first action after PENDING", func(t *testing.T) { + statuses.Store(&[]string{pending, pending, inProgress, `"action": "NO_ACTION", "latestRunUuid": "run-2"`}) - _, block, err := wait.AwaitReaction(t.Context(), before) + _, block, err := wait.AwaitStart(t.Context()) require.NoError(t, err) - assert.Equal(t, "preflight-1", block.Status.GetLatestDryRunUuid()) + assert.Equal(t, client.BuildingBlockActionInProgress, block.Status.Action) }) - t.Run("fails when meshStack does not react within the start window", func(t *testing.T) { - status.Store(new(`"action": "NO_ACTION", "latestRunUuid": "run-1"`)) + t.Run("the end waits out PENDING and IN_PROGRESS", func(t *testing.T) { + statuses.Store(&[]string{pending, inProgress, inProgress, `"action": "REVIEW_ERRORS", "latestRunUuid": "run-2"`}) - _, _, err := wait.AwaitReaction(t.Context(), before) + _, block, err := wait.AwaitEnd(t.Context()) - require.ErrorContains(t, err, "meshStack did not react to the write of building block "+buildingBlockUuid.String()+" within 50ms") - require.ErrorContains(t, err, "https://panel.example/block") + require.NoError(t, err) + require.ErrorContains(t, wait.Outcome(block), "ended with action REVIEW_ERRORS") }) t.Run("fails with the reason meshStack could not start the run", func(t *testing.T) { - status.Store(new(`"action": "REVIEW_ERRORS", "latestRunUuid": "run-1", "runStartFailure": "no runner", "runStartFailedOn": "2026-10-29T10:00:00Z"`)) + statuses.Store(&[]string{pending, `"action": "REVIEW_ERRORS", "latestRunUuid": "run-1", "runStartFailure": "no runner", "runStartFailedOn": "2026-10-29T10:00:00Z"`}) - _, block, err := wait.AwaitReaction(t.Context(), before) + _, block, err := wait.AwaitStart(t.Context()) require.NoError(t, err) require.ErrorContains(t, wait.StartFailure(block), "meshStack could not start the run of building block "+buildingBlockUuid.String()+": no runner") + require.ErrorContains(t, wait.StartFailure(block), "https://panel.example/block") }) } diff --git a/cmd/internal/testacc/buildingblock_test.go b/cmd/internal/testacc/buildingblock_test.go index 21e12a9..55c4f13 100644 --- a/cmd/internal/testacc/buildingblock_test.go +++ b/cmd/internal/testacc/buildingblock_test.go @@ -88,11 +88,11 @@ func waitEndsTheRun(c *cli) func(*testing.T) { requireEnded(t, output, err) }) - t.Run("wait --before=- with the answer of a write through api", func(t *testing.T) { + t.Run("wait after a write through api", func(t *testing.T) { write := c.start(`{"dryRun": false}`, "api", "-X", "POST", "/api/meshobjects/meshbuildingblocks/"+buildingBlock+"/trigger-run", "--request-json", "-") require.NoErrorf(t, write.wait(), "the trigger-run through api failed:\n%s", write.output) - output, err := c.run(write.stdout.String(), "buildingblock", "wait", buildingBlock, "--before=-", "-o", "ndjson") + output, err := c.run("", "buildingblock", "wait", buildingBlock, "-o", "ndjson") requireEnded(t, output, err) }) } diff --git a/skills/meshstack-cli/SKILL.md b/skills/meshstack-cli/SKILL.md index 50b8764..972b815 100644 --- a/skills/meshstack-cli/SKILL.md +++ b/skills/meshstack-cli/SKILL.md @@ -28,7 +28,7 @@ skill and the help disagree, trust the help. | `meshstack workspace list` | | workspaces this login can see | | `meshstack buildingblock list` | `bb` | building blocks (deployed instances), newest first | | `meshstack buildingblock trigger-run ` | `bb` | starts a run of one block | -| `meshstack buildingblock wait --before ` | `bb` | waits until the run of one block has ended | +| `meshstack buildingblock wait ` | `bb` | waits until the run of one block has ended | | `meshstack buildingblock tfstate` | `bb` | reads and writes the OpenTofu state of one block | | `meshstack buildingblockdefinition list` | `bbd` | definitions — what platform teams offer | | `meshstack buildingblockdefinitionversion list --definition ` | `bbdv` | the versions of one definition | @@ -357,13 +357,13 @@ Start a run and wait until it has ended: meshstack bb trigger-run --wait ``` -After a write through `meshstack api`, pipe its answer into `bb wait`. meshStack answers before it -starts the run, so the command compares the block with that answer, by the rules of -[Waiting for a Run](https://docs.meshcloud.io/concepts/building-block/#waiting-for-a-run): +After a write through `meshstack api`, run `bb wait`. From a write that asks for a run, meshStack +shows the action `PENDING` until it has started the run, so the command waits while the action is +`PENDING` or `IN_PROGRESS`: ```sh -meshstack api -X PUT /api/meshobjects/meshbuildingblocks/ --request-json block.json | - meshstack bb wait --before=- +meshstack api -X PUT /api/meshobjects/meshbuildingblocks/ --request-json block.json && + meshstack bb wait ``` Both fail, and name the action, when the run failed or waits for a person or a dependency.