Skip to content
Closed
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
20 changes: 15 additions & 5 deletions client/building_block_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,21 @@ func (bb *MeshBuildingBlockV2) DeletionSuccessful() (done bool, err error) {
return
}

func (c meshBuildingBlockV2Client) TriggerRun(ctx context.Context, bbUuid string) (err error) {
// MeshBuildingBlockV2TriggerRunRequest is the body of a trigger-run. DryRun asks for a DETECT run,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove useless comment, these are already covered by API docs authoritatively and should not be restated in the clinet code

// which plans the block without changing it.
type MeshBuildingBlockV2TriggerRunRequest struct {
// dryRun is not optional to the endpoint once a body is sent, so it goes out as false rather
// than being omitted.
_, err = c.meshObject.PostAtPath[any](ctx, struct {
DryRun bool `json:"dryRun"`
}{}, bbUuid, "trigger-run")
return
DryRun bool `json:"dryRun"`
}

func (c meshBuildingBlockV2Client) TriggerRun(ctx context.Context, bbUuid string) error {
_, err := c.TriggerRunWith(ctx, bbUuid, MeshBuildingBlockV2TriggerRunRequest{})
return err
}

// TriggerRunWith returns the building block as meshStack answers the trigger, undecoded. The run itself starts
// asynchronously, so its status.latestRunUuid and status.latestDryRunUuid may still name an older run.
func (c meshBuildingBlockV2Client) TriggerRunWith(ctx context.Context, bbUuid string, request MeshBuildingBlockV2TriggerRunRequest) (jsontext.Value, error) {
return c.meshObject.PostAtPath[jsontext.Value](ctx, request, bbUuid, "trigger-run")
}
3 changes: 3 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Client struct {
WorkspaceGroupBinding MeshWorkspaceGroupBindingClient
WorkspaceUserBinding MeshWorkspaceUserBindingClient
Listing MeshListingClient
RunTrigger MeshRunTriggerClient

// Endpoint is read by the Terraform provider's meshstack_instance data source.
Endpoint xurl.URL
Expand Down Expand Up @@ -94,6 +95,8 @@ func New(ctx context.Context, endpoint xurl.URL, userAgent string, auth Authoriz
buildingBlockDefinitionVersion: buildingBlockDefinitionVersion,
},

RunTrigger: meshRunTriggerClient{buildingBlockV2: buildingBlockV2},

Endpoint: endpoint,
}
}
14 changes: 14 additions & 0 deletions client/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,17 @@ func (c meshListingClient) BuildingBlockDefinitionVersions(ctx context.Context,
func (c meshListingClient) BuildingBlockRunLogs(ctx context.Context, runUuid string) (jsontext.Value, error) {
return c.buildingBlockRun.GetLogsRaw(ctx, runUuid)
}

// MeshRunTriggerClient is a client of the CLI alone: it stays off [MeshBuildingBlockV2Client], whose
// every method the Terraform provider's mocks implement.
type MeshRunTriggerClient interface {
TriggerBuildingBlockRun(ctx context.Context, buildingBlockUuid string, request MeshBuildingBlockV2TriggerRunRequest) (jsontext.Value, error)
}

type meshRunTriggerClient struct {
buildingBlockV2 meshBuildingBlockV2Client
}

func (c meshRunTriggerClient) TriggerBuildingBlockRun(ctx context.Context, buildingBlockUuid string, request MeshBuildingBlockV2TriggerRunRequest) (jsontext.Value, error) {
return c.buildingBlockV2.TriggerRunWith(ctx, buildingBlockUuid, request)
}
1 change: 1 addition & 0 deletions cmd/buildingblock/buildingblock.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func New() *cobra.Command {
}

cmd.AddCommand(newList())
cmd.AddCommand(newTriggerRun())

return cmd
}
54 changes: 54 additions & 0 deletions cmd/buildingblock/trigger_run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package buildingblock

import (
"fmt"
"log/slog"

"github.com/spf13/cobra"

"github.com/meshcloud/meshstack-cli/client"
"github.com/meshcloud/meshstack-cli/cmd/internal"
)

func newTriggerRun() *cobra.Command {
var (
output internal.OutputFlag
dryRun bool
)

cmd := &cobra.Command{
Use: "trigger-run <building-block-uuid>",
Short: "Ask meshStack to run a building block",
Long: `Ask meshStack to run a building block, and write the building block meshStack answers with.

--dry-run asks for a dry (DETECT) run, which plans the block without changing it.

The command does not wait for the run. meshStack starts it asynchronously, and holds it until a
person approves it in meshPanel if the definition asks for approval. The status.latestRunUuid and
status.latestDryRunUuid of the answer may therefore still name an older run.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
meshStack, err := internal.ResolveClient(ctx)
if err != nil {
return err
}
buildingBlockUuid := args[0]
buildingBlock, err := meshStack.RunTrigger.TriggerBuildingBlockRun(ctx, buildingBlockUuid, client.MeshBuildingBlockV2TriggerRunRequest{DryRun: dryRun})
if err != nil {
return err
}
if err := internal.WriteItem(cmd.OutOrStdout(), output.Format, buildingBlock); err != nil {
return err
}
slog.InfoContext(ctx, fmt.Sprintf("meshStack accepted a run of building block %s. Follow it with `meshstack buildingblockrun list --building-block %s`",
buildingBlockUuid, buildingBlockUuid))
return nil
},
}

cmd.Flags().BoolVar(&dryRun, "dry-run", false, "ask for a dry (DETECT) run that changes nothing")
output.Register(cmd.Flags())

return cmd
}
72 changes: 72 additions & 0 deletions cmd/buildingblock/trigger_run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package buildingblock_test

import (
"bytes"
"io"
gohttp "net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/meshcloud/meshstack-cli/cmd/buildingblock"
"github.com/meshcloud/meshstack-cli/pkg/setting"
)

const (
buildingBlockUuid = "b1d2c3e4-0000-4000-8000-000000000001"
triggerRunPath = "/api/meshobjects/meshbuildingblocks/" + buildingBlockUuid + "/trigger-run"
// unexpiringJwt is an unsigned JWT that expires in 2100, so the CLI sends it rather than asking for a new one.
unexpiringJwt = "eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9."
)

func TestTriggerRunSendsDryRunAndWritesTheAnsweredBuildingBlock(t *testing.T) {
for _, tt := range []struct {
args []string
wantBody string
}{
{args: nil, wantBody: `"dryRun":false`},
{args: []string{"--dry-run"}, wantBody: `"dryRun":true`},
} {
t.Run(tt.wantBody, func(t *testing.T) {
var requests []string
meshStack := httptest.NewServer(gohttp.HandlerFunc(func(w gohttp.ResponseWriter, r *gohttp.Request) {
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
requests = append(requests, r.Method+" "+r.URL.Path+" "+string(body))
if r.Method != gohttp.MethodPost || r.URL.Path != triggerRunPath {
gohttp.NotFound(w, r)
return
}
w.WriteHeader(gohttp.StatusAccepted)
_, _ = io.WriteString(w, `{"metadata":{"uuid":"`+buildingBlockUuid+`"},"status":{"status":"PENDING","latestRunUuid":"r1"}}`)
}))
t.Cleanup(meshStack.Close)
useFakeMeshStack(t, meshStack.URL)

var stdout bytes.Buffer
cmd := buildingblock.New()
cmd.SetArgs(append([]string{"trigger-run", buildingBlockUuid, "-o", "ndjson"}, tt.args...))
cmd.SetOut(&stdout)
require.NoError(t, cmd.ExecuteContext(t.Context()))

require.Len(t, requests, 1, "the command triggers the run and does nothing else, such as waiting for it")
assert.Contains(t, requests[0], "POST "+triggerRunPath)
assert.Contains(t, requests[0], tt.wantBody)
assert.Contains(t, stdout.String(), `"latestRunUuid":"r1"`)
})
}
}

func useFakeMeshStack(t *testing.T, endpoint string) {
t.Helper()
t.Setenv("MESHSTACK_CONFIG_DIR", t.TempDir())
t.Setenv(setting.Endpoint.EnvKey(), endpoint)
t.Setenv(setting.SkipVersionCheck.EnvKey(), "true")
t.Setenv(setting.Profile.EnvKey(), "")
t.Setenv(setting.Workspace.EnvKey(), "")
t.Setenv(setting.ApiKeyClientId.EnvKey(), "")
t.Setenv(setting.ApiKeyClientSecret.EnvKey(), "")
t.Setenv(setting.ApiToken.EnvKey(), unexpiringJwt)
}
Loading