From 95e01c783f7668440cd1c722b178b0bae0a9ee4b Mon Sep 17 00:00:00 2001 From: Jeroen van Erp Date: Tue, 17 Mar 2026 11:29:27 +0100 Subject: [PATCH 1/4] Query Topology Health states Signed-off-by: Jeroen van Erp --- cmd/topology.go | 1 + cmd/topology/topology_state.go | 234 ++++++++++++++++++++++ cmd/topology/topology_state_test.go | 291 ++++++++++++++++++++++++++++ 3 files changed, 526 insertions(+) create mode 100644 cmd/topology/topology_state.go create mode 100644 cmd/topology/topology_state_test.go diff --git a/cmd/topology.go b/cmd/topology.go index 0bf400be..6fa1006b 100644 --- a/cmd/topology.go +++ b/cmd/topology.go @@ -13,6 +13,7 @@ func TopologyCommand(cli *di.Deps) *cobra.Command { Long: "Inspect SUSE Observability topology components. Query and display topology components using component types, tags, and identifiers.", } cmd.AddCommand(topology.InspectCommand(cli)) + cmd.AddCommand(topology.StateCommand(cli)) return cmd } diff --git a/cmd/topology/topology_state.go b/cmd/topology/topology_state.go new file mode 100644 index 00000000..35147c0a --- /dev/null +++ b/cmd/topology/topology_state.go @@ -0,0 +1,234 @@ +package topology + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/stackvista/stackstate-cli/generated/stackstate_api" + stscobra "github.com/stackvista/stackstate-cli/internal/cobra" + "github.com/stackvista/stackstate-cli/internal/common" + "github.com/stackvista/stackstate-cli/internal/di" + "github.com/stackvista/stackstate-cli/internal/printer" +) + +type StateArgs struct { + ComponentType string + Tags []string + Identifiers []string + Limit int + STQL string +} + +func StateCommand(cli *di.Deps) *cobra.Command { + args := &StateArgs{} + + cmd := &cobra.Command{ + Use: "state", + Short: "Show the health state of topology components", + Long: "Show the health state of topology components by type, tags, and identifiers. Displays the health state for each matching component.", + Example: `# show state of components of a specific type +sts topology state --type "otel service instance" + +# show state with tag filtering +sts topology state --type "otel service instance" --tag "service.namespace:opentelemetry-demo-demo-dev" + +# show state with multiple tags (ANDed) +sts topology state --type "otel service instance" \ + --tag "service.namespace:opentelemetry-demo-demo-dev" \ + --tag "service.name:accountingservice" + +# show state with identifier filtering +sts topology state --type "otel service instance" --identifier "urn:opentelemetry:..." + +# show state with limit on number of results +sts topology state --type "otel service instance" --limit 10 + +# show state and display as JSON +sts topology state --type "otel service instance" -o json + +# show state using a custom STQL query +sts topology state --stql 'type = "otel service instance" AND healthState = "CRITICAL"'`, + RunE: cli.CmdRunEWithApi(func(cmd *cobra.Command, cli *di.Deps, api *stackstate_api.APIClient, serverInfo *stackstate_api.ServerInfo) common.CLIError { + return RunStateCommand(cmd, cli, api, serverInfo, args) + }), + } + + cmd.Flags().StringVar(&args.ComponentType, "type", "", "Component type") + cmd.Flags().StringSliceVar(&args.Tags, "tag", []string{}, "Filter by tags in format 'tag-name:tag-value' (multiple allowed, ANDed together)") + cmd.Flags().StringSliceVar(&args.Identifiers, "identifier", []string{}, "Filter by component identifiers (multiple allowed, ANDed together)") + cmd.Flags().IntVar(&args.Limit, "limit", 0, "Maximum number of components to output (must be positive)") + cmd.Flags().StringVar(&args.STQL, "stql", "", "STQL query to select components (mutually exclusive with --type, --tag, --identifier)") + stscobra.MarkMutexFlags(cmd, []string{"type", "stql"}, "query", true) + + return cmd +} + +func RunStateCommand( + _ *cobra.Command, + cli *di.Deps, + api *stackstate_api.APIClient, + _ *stackstate_api.ServerInfo, + args *StateArgs, +) common.CLIError { + if args.Limit < 0 { + return common.NewExecutionError(fmt.Errorf("limit must be a positive number, got: %d", args.Limit)) + } + + var query string + if args.STQL != "" { + query = args.STQL + } else { + query = buildSTQLQuery(args.ComponentType, args.Tags, args.Identifiers) + } + + metadata := stackstate_api.NewQueryMetadata( + false, + false, + 0, + false, + false, + false, + false, + false, + false, + true, + ) + + request := stackstate_api.NewViewSnapshotRequest( + "SnapshotRequest", + query, + "0.0.1", + *metadata, + ) + + result, resp, err := api.SnapshotApi.QuerySnapshot(cli.Context). + ViewSnapshotRequest(*request). + Execute() + if err != nil { + return common.NewResponseError(err, resp) + } + + componentStates, parseErr := parseStateResponse(result) + if parseErr != nil { + if typedErr := handleSnapshotError(result.ViewSnapshotResponse, resp); typedErr != nil { + return typedErr + } + return common.NewExecutionError(parseErr) + } + + // Apply limit if specified + if args.Limit > 0 && len(componentStates) > args.Limit { + componentStates = componentStates[:args.Limit] + } + + if cli.IsJson() { + cli.Printer.PrintJson(map[string]interface{}{ + "components": componentStates, + }) + return nil + } else { + printStateTableOutput(cli, componentStates) + } + + return nil +} + +// ComponentState holds the component information along with its health state. +type ComponentState struct { + ID int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Identifiers []string `json:"identifiers"` + HealthState string `json:"healthState"` +} + +func parseStateResponse(result *stackstate_api.QuerySnapshotResult) ([]ComponentState, error) { + respMap := result.ViewSnapshotResponse + if respMap == nil { + return nil, fmt.Errorf("response data is nil") + } + + respType, ok := respMap["_type"].(string) + if !ok { + return nil, fmt.Errorf("response has no _type discriminator") + } + + if respType != "ViewSnapshot" { + return nil, fmt.Errorf("response is an error type: %s", respType) + } + + metadata := parseMetadata(respMap) + + var componentStates []ComponentState + if componentsSlice, ok := respMap["components"].([]interface{}); ok { + for _, comp := range componentsSlice { + if compMap, ok := comp.(map[string]interface{}); ok { + componentStates = append(componentStates, parseComponentStateFromMap(compMap, metadata)) + } + } + } + + return componentStates, nil +} + +func parseComponentStateFromMap(compMap map[string]interface{}, metadata ComponentMetadata) ComponentState { + cs := ComponentState{ + Identifiers: []string{}, + HealthState: "UNKNOWN", + } + + // Parse basic fields + if id, ok := compMap["id"].(float64); ok { + cs.ID = int64(id) + } + if name, ok := compMap["name"].(string); ok { + cs.Name = name + } + + // Parse type + if typeID, ok := compMap["type"].(float64); ok { + if typeName, found := metadata.ComponentTypes[int64(typeID)]; found { + cs.Type = typeName + } else { + cs.Type = fmt.Sprintf("Unknown (%d)", int64(typeID)) + } + } + + // Parse identifiers + if identifiersRaw, ok := compMap["identifiers"].([]interface{}); ok { + for _, idRaw := range identifiersRaw { + if id, ok := idRaw.(string); ok { + cs.Identifiers = append(cs.Identifiers, id) + } + } + } + + // Parse health state from state.healthState + if stateMap, ok := compMap["state"].(map[string]interface{}); ok { + if healthState, ok := stateMap["healthState"].(string); ok { + cs.HealthState = healthState + } + } + + return cs +} + +func printStateTableOutput(cli *di.Deps, componentStates []ComponentState) { + var tableData [][]interface{} + for _, cs := range componentStates { + identifiersStr := strings.Join(cs.Identifiers, ", ") + tableData = append(tableData, []interface{}{ + cs.Name, + cs.Type, + cs.HealthState, + identifiersStr, + }) + } + + cli.Printer.Table(printer.TableData{ + Header: []string{"Name", "Type", "Health State", "Identifiers"}, + Data: tableData, + MissingTableDataMsg: printer.NotFoundMsg{Types: "components"}, + }) +} diff --git a/cmd/topology/topology_state_test.go b/cmd/topology/topology_state_test.go new file mode 100644 index 00000000..3fff656f --- /dev/null +++ b/cmd/topology/topology_state_test.go @@ -0,0 +1,291 @@ +package topology + +import ( + "testing" + + "github.com/spf13/cobra" + sts "github.com/stackvista/stackstate-cli/generated/stackstate_api" + "github.com/stackvista/stackstate-cli/internal/di" + "github.com/stackvista/stackstate-cli/internal/printer" + "github.com/stretchr/testify/assert" +) + +func setStateCmd(t *testing.T) (*di.MockDeps, *cobra.Command) { + cli := di.NewMockDeps(t) + cmd := StateCommand(&cli.Deps) + return &cli, cmd +} + +func mockSnapshotResponseWithState() sts.QuerySnapshotResult { + return sts.QuerySnapshotResult{ + Type: "QuerySnapshotResult", + ViewSnapshotResponse: map[string]interface{}{ + "_type": "ViewSnapshot", + "components": []interface{}{ + map[string]interface{}{ + "id": float64(229404307680647), + "name": "test-component", + "type": float64(239975151751041), + "layer": float64(186771622698247), + "domain": float64(209616858431909), + "identifiers": []interface{}{"urn:test:component:1"}, + "tags": []interface{}{"service.namespace:test"}, + "state": map[string]interface{}{ + "healthState": "CRITICAL", + }, + }, + }, + "metadata": map[string]interface{}{ + "componentTypes": []interface{}{ + map[string]interface{}{ + "id": float64(239975151751041), + "name": "test type", + }, + }, + "layers": []interface{}{ + map[string]interface{}{ + "id": float64(186771622698247), + "name": "Test Layer", + }, + }, + "domains": []interface{}{ + map[string]interface{}{ + "id": float64(209616858431909), + "name": "Test Domain", + }, + }, + }, + }, + } +} + +func TestTopologyStateJson(t *testing.T) { + cli, cmd := setStateCmd(t) + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponseWithState() + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--type", "test type", "-o", "json") + + // Assert API was called + calls := *cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotCalls + assert.Len(t, calls, 1) + + // Assert JSON output contains components + jsonCalls := *cli.MockPrinter.PrintJsonCalls + assert.Len(t, jsonCalls, 1) + + jsonData := jsonCalls[0] + assert.NotNil(t, jsonData["components"]) + + // Check the component has the health state + components := jsonData["components"].([]ComponentState) + assert.Len(t, components, 1) + assert.Equal(t, "CRITICAL", components[0].HealthState) +} + +func TestTopologyStateTable(t *testing.T) { + cli, cmd := setStateCmd(t) + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponseWithState() + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--type", "test type") + + // Assert API was called + calls := *cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotCalls + assert.Len(t, calls, 1) + + // Assert table output + tableCalls := *cli.MockPrinter.TableCalls + assert.Len(t, tableCalls, 1) + + table := tableCalls[0] + assert.Equal(t, []string{"Name", "Type", "Health State", "Identifiers"}, table.Header) + assert.Len(t, table.Data, 1) + + row := table.Data[0] + assert.Equal(t, "test-component", row[0]) + assert.Equal(t, "test type", row[1]) + assert.Equal(t, "CRITICAL", row[2]) + assert.Equal(t, "urn:test:component:1", row[3]) +} + +func TestTopologyStateWithTags(t *testing.T) { + cli, cmd := setStateCmd(t) + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponseWithState() + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, + "--type", "test type", + "--tag", "service.namespace:test", + "--tag", "service.name:myservice") + + calls := *cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotCalls + assert.Len(t, calls, 1) + + // Verify query contains both tags ANDed + call := calls[0] + request := call.PviewSnapshotRequest + expectedQuery := `type = "test type" AND label = "service.namespace:test" AND label = "service.name:myservice"` + assert.Equal(t, expectedQuery, request.Query) +} + +func TestTopologyStateWithIdentifiers(t *testing.T) { + cli, cmd := setStateCmd(t) + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponseWithState() + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, + "--type", "test type", + "--identifier", "urn:test:1", + "--identifier", "urn:test:2") + + calls := *cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotCalls + assert.Len(t, calls, 1) + + // Verify query contains identifiers in IN clause + call := calls[0] + request := call.PviewSnapshotRequest + expectedQuery := `type = "test type" AND identifier IN ("urn:test:1", "urn:test:2")` + assert.Equal(t, expectedQuery, request.Query) +} + +func TestTopologyStateNoResults(t *testing.T) { + cli, cmd := setStateCmd(t) + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = sts.QuerySnapshotResult{ + Type: "QuerySnapshotResult", + ViewSnapshotResponse: map[string]interface{}{ + "_type": "ViewSnapshot", + "components": []interface{}{}, + "metadata": map[string]interface{}{ + "componentTypes": []interface{}{}, + "layers": []interface{}{}, + "domains": []interface{}{}, + }, + }, + } + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--type", "nonexistent") + + // Verify empty table is displayed + tableCalls := *cli.MockPrinter.TableCalls + assert.Len(t, tableCalls, 1) + + table := tableCalls[0] + assert.Equal(t, []string{"Name", "Type", "Health State", "Identifiers"}, table.Header) + assert.Len(t, table.Data, 0) + assert.Equal(t, printer.NotFoundMsg{Types: "components"}, table.MissingTableDataMsg) +} + +func TestTopologyStateDefaultsToUnknown(t *testing.T) { + cli, cmd := setStateCmd(t) + // Response without state field + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = sts.QuerySnapshotResult{ + Type: "QuerySnapshotResult", + ViewSnapshotResponse: map[string]interface{}{ + "_type": "ViewSnapshot", + "components": []interface{}{ + map[string]interface{}{ + "id": float64(229404307680647), + "name": "test-component", + "type": float64(239975151751041), + "identifiers": []interface{}{"urn:test:component:1"}, + // No state field + }, + }, + "metadata": map[string]interface{}{ + "componentTypes": []interface{}{ + map[string]interface{}{ + "id": float64(239975151751041), + "name": "test type", + }, + }, + }, + }, + } + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--type", "test type") + + tableCalls := *cli.MockPrinter.TableCalls + assert.Len(t, tableCalls, 1) + + table := tableCalls[0] + row := table.Data[0] + assert.Equal(t, "UNKNOWN", row[2]) // Health state defaults to UNKNOWN +} + +func TestTopologyStateLimit(t *testing.T) { + cli, cmd := setStateCmd(t) + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = sts.QuerySnapshotResult{ + Type: "QuerySnapshotResult", + ViewSnapshotResponse: map[string]interface{}{ + "_type": "ViewSnapshot", + "components": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "name": "component-1", + "type": float64(239975151751041), + "identifiers": []interface{}{"urn:test:1"}, + "state": map[string]interface{}{ + "healthState": "CLEAR", + }, + }, + map[string]interface{}{ + "id": float64(2), + "name": "component-2", + "type": float64(239975151751041), + "identifiers": []interface{}{"urn:test:2"}, + "state": map[string]interface{}{ + "healthState": "CRITICAL", + }, + }, + map[string]interface{}{ + "id": float64(3), + "name": "component-3", + "type": float64(239975151751041), + "identifiers": []interface{}{"urn:test:3"}, + "state": map[string]interface{}{ + "healthState": "DEVIATING", + }, + }, + }, + "metadata": map[string]interface{}{ + "componentTypes": []interface{}{ + map[string]interface{}{ + "id": float64(239975151751041), + "name": "test type", + }, + }, + }, + }, + } + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--type", "test type", "--limit", "2") + + tableCalls := *cli.MockPrinter.TableCalls + assert.Len(t, tableCalls, 1) + + table := tableCalls[0] + assert.Len(t, table.Data, 2) // Only 2 components due to limit +} + +func TestTopologyStateWithSTQL(t *testing.T) { + cli, cmd := setStateCmd(t) + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponseWithState() + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--stql", `type = "custom" AND healthState = "CRITICAL"`) + + calls := *cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotCalls + assert.Len(t, calls, 1) + + // Verify the custom STQL query is used directly + call := calls[0] + request := call.PviewSnapshotRequest + assert.Equal(t, `type = "custom" AND healthState = "CRITICAL"`, request.Query) +} + +func TestTopologyStateSTQLMutuallyExclusiveWithType(t *testing.T) { + cli, cmd := setStateCmd(t) + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponseWithState() + + _, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--type", "test type", "--stql", "type = \"test\"") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "type") + assert.Contains(t, err.Error(), "stql") +} From 739a6d0f2d243a3691a81721eabe6229fb9859ee Mon Sep 17 00:00:00 2001 From: Jeroen van Erp Date: Tue, 17 Mar 2026 11:44:38 +0100 Subject: [PATCH 2/4] Removed test fixture duplication --- cmd/topology/topology_inspect_test.go | 41 -------------------- cmd/topology/topology_state_test.go | 55 +++------------------------ cmd/topology/topology_test_helper.go | 48 +++++++++++++++++++++++ 3 files changed, 54 insertions(+), 90 deletions(-) create mode 100644 cmd/topology/topology_test_helper.go diff --git a/cmd/topology/topology_inspect_test.go b/cmd/topology/topology_inspect_test.go index eb8fb635..62d40503 100644 --- a/cmd/topology/topology_inspect_test.go +++ b/cmd/topology/topology_inspect_test.go @@ -18,47 +18,6 @@ func setInspectCmd(t *testing.T) (*di.MockDeps, *cobra.Command) { return &cli, cmd } -func mockSnapshotResponse() sts.QuerySnapshotResult { - return sts.QuerySnapshotResult{ - Type: "QuerySnapshotResult", - ViewSnapshotResponse: map[string]interface{}{ - "_type": "ViewSnapshot", - "components": []interface{}{ - map[string]interface{}{ - "id": float64(229404307680647), - "name": "test-component", - "type": float64(239975151751041), - "layer": float64(186771622698247), - "domain": float64(209616858431909), - "identifiers": []interface{}{"urn:test:component:1"}, - "tags": []interface{}{"service.namespace:test"}, - "properties": map[string]interface{}{"key": "value"}, - }, - }, - "metadata": map[string]interface{}{ - "componentTypes": []interface{}{ - map[string]interface{}{ - "id": float64(239975151751041), - "name": "test type", - }, - }, - "layers": []interface{}{ - map[string]interface{}{ - "id": float64(186771622698247), - "name": "Test Layer", - }, - }, - "domains": []interface{}{ - map[string]interface{}{ - "id": float64(209616858431909), - "name": "Test Domain", - }, - }, - }, - }, - } -} - func TestTopologyInspectJson(t *testing.T) { cli, cmd := setInspectCmd(t) cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponse() diff --git a/cmd/topology/topology_state_test.go b/cmd/topology/topology_state_test.go index 3fff656f..38c149f9 100644 --- a/cmd/topology/topology_state_test.go +++ b/cmd/topology/topology_state_test.go @@ -16,52 +16,9 @@ func setStateCmd(t *testing.T) (*di.MockDeps, *cobra.Command) { return &cli, cmd } -func mockSnapshotResponseWithState() sts.QuerySnapshotResult { - return sts.QuerySnapshotResult{ - Type: "QuerySnapshotResult", - ViewSnapshotResponse: map[string]interface{}{ - "_type": "ViewSnapshot", - "components": []interface{}{ - map[string]interface{}{ - "id": float64(229404307680647), - "name": "test-component", - "type": float64(239975151751041), - "layer": float64(186771622698247), - "domain": float64(209616858431909), - "identifiers": []interface{}{"urn:test:component:1"}, - "tags": []interface{}{"service.namespace:test"}, - "state": map[string]interface{}{ - "healthState": "CRITICAL", - }, - }, - }, - "metadata": map[string]interface{}{ - "componentTypes": []interface{}{ - map[string]interface{}{ - "id": float64(239975151751041), - "name": "test type", - }, - }, - "layers": []interface{}{ - map[string]interface{}{ - "id": float64(186771622698247), - "name": "Test Layer", - }, - }, - "domains": []interface{}{ - map[string]interface{}{ - "id": float64(209616858431909), - "name": "Test Domain", - }, - }, - }, - }, - } -} - func TestTopologyStateJson(t *testing.T) { cli, cmd := setStateCmd(t) - cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponseWithState() + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponse() di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--type", "test type", "-o", "json") @@ -84,7 +41,7 @@ func TestTopologyStateJson(t *testing.T) { func TestTopologyStateTable(t *testing.T) { cli, cmd := setStateCmd(t) - cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponseWithState() + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponse() di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--type", "test type") @@ -109,7 +66,7 @@ func TestTopologyStateTable(t *testing.T) { func TestTopologyStateWithTags(t *testing.T) { cli, cmd := setStateCmd(t) - cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponseWithState() + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponse() di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--type", "test type", @@ -128,7 +85,7 @@ func TestTopologyStateWithTags(t *testing.T) { func TestTopologyStateWithIdentifiers(t *testing.T) { cli, cmd := setStateCmd(t) - cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponseWithState() + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponse() di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--type", "test type", @@ -266,7 +223,7 @@ func TestTopologyStateLimit(t *testing.T) { func TestTopologyStateWithSTQL(t *testing.T) { cli, cmd := setStateCmd(t) - cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponseWithState() + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponse() di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--stql", `type = "custom" AND healthState = "CRITICAL"`) @@ -281,7 +238,7 @@ func TestTopologyStateWithSTQL(t *testing.T) { func TestTopologyStateSTQLMutuallyExclusiveWithType(t *testing.T) { cli, cmd := setStateCmd(t) - cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponseWithState() + cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponse() _, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--type", "test type", "--stql", "type = \"test\"") diff --git a/cmd/topology/topology_test_helper.go b/cmd/topology/topology_test_helper.go new file mode 100644 index 00000000..22fd13db --- /dev/null +++ b/cmd/topology/topology_test_helper.go @@ -0,0 +1,48 @@ +package topology + +import ( + sts "github.com/stackvista/stackstate-cli/generated/stackstate_api" +) + +func mockSnapshotResponse() sts.QuerySnapshotResult { + return sts.QuerySnapshotResult{ + Type: "QuerySnapshotResult", + ViewSnapshotResponse: map[string]interface{}{ + "_type": "ViewSnapshot", + "components": []interface{}{ + map[string]interface{}{ + "id": float64(229404307680647), + "name": "test-component", + "type": float64(239975151751041), + "layer": float64(186771622698247), + "domain": float64(209616858431909), + "identifiers": []interface{}{"urn:test:component:1"}, + "tags": []interface{}{"service.namespace:test"}, + "state": map[string]interface{}{ + "healthState": "CRITICAL", + }, + }, + }, + "metadata": map[string]interface{}{ + "componentTypes": []interface{}{ + map[string]interface{}{ + "id": float64(239975151751041), + "name": "test type", + }, + }, + "layers": []interface{}{ + map[string]interface{}{ + "id": float64(186771622698247), + "name": "Test Layer", + }, + }, + "domains": []interface{}{ + map[string]interface{}{ + "id": float64(209616858431909), + "name": "Test Domain", + }, + }, + }, + }, + } +} From 8a5801a9284b6a85d342b1d65ed2bf4930227e08 Mon Sep 17 00:00:00 2001 From: Jeroen van Erp Date: Tue, 17 Mar 2026 11:53:56 +0100 Subject: [PATCH 3/4] Ignore magic timestamps --- cmd/topology/topology_test_helper.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/topology/topology_test_helper.go b/cmd/topology/topology_test_helper.go index 22fd13db..217dd3c6 100644 --- a/cmd/topology/topology_test_helper.go +++ b/cmd/topology/topology_test_helper.go @@ -4,6 +4,7 @@ import ( sts "github.com/stackvista/stackstate-cli/generated/stackstate_api" ) +//nolint:mnd func mockSnapshotResponse() sts.QuerySnapshotResult { return sts.QuerySnapshotResult{ Type: "QuerySnapshotResult", From a3a9f8fca816af2527e73ae24b9d5645c71d76a2 Mon Sep 17 00:00:00 2001 From: Jeroen van Erp Date: Wed, 18 Mar 2026 09:55:15 +0100 Subject: [PATCH 4/4] Remove stql option Signed-off-by: Jeroen van Erp --- cmd/topology/topology_state.go | 16 ++-------------- cmd/topology/topology_state_test.go | 26 -------------------------- 2 files changed, 2 insertions(+), 40 deletions(-) diff --git a/cmd/topology/topology_state.go b/cmd/topology/topology_state.go index 35147c0a..9f1f1397 100644 --- a/cmd/topology/topology_state.go +++ b/cmd/topology/topology_state.go @@ -6,7 +6,6 @@ import ( "github.com/spf13/cobra" "github.com/stackvista/stackstate-cli/generated/stackstate_api" - stscobra "github.com/stackvista/stackstate-cli/internal/cobra" "github.com/stackvista/stackstate-cli/internal/common" "github.com/stackvista/stackstate-cli/internal/di" "github.com/stackvista/stackstate-cli/internal/printer" @@ -17,7 +16,6 @@ type StateArgs struct { Tags []string Identifiers []string Limit int - STQL string } func StateCommand(cli *di.Deps) *cobra.Command { @@ -45,10 +43,7 @@ sts topology state --type "otel service instance" --identifier "urn:opentelemetr sts topology state --type "otel service instance" --limit 10 # show state and display as JSON -sts topology state --type "otel service instance" -o json - -# show state using a custom STQL query -sts topology state --stql 'type = "otel service instance" AND healthState = "CRITICAL"'`, +sts topology state --type "otel service instance" -o json`, RunE: cli.CmdRunEWithApi(func(cmd *cobra.Command, cli *di.Deps, api *stackstate_api.APIClient, serverInfo *stackstate_api.ServerInfo) common.CLIError { return RunStateCommand(cmd, cli, api, serverInfo, args) }), @@ -58,8 +53,6 @@ sts topology state --stql 'type = "otel service instance" AND healthState = "CRI cmd.Flags().StringSliceVar(&args.Tags, "tag", []string{}, "Filter by tags in format 'tag-name:tag-value' (multiple allowed, ANDed together)") cmd.Flags().StringSliceVar(&args.Identifiers, "identifier", []string{}, "Filter by component identifiers (multiple allowed, ANDed together)") cmd.Flags().IntVar(&args.Limit, "limit", 0, "Maximum number of components to output (must be positive)") - cmd.Flags().StringVar(&args.STQL, "stql", "", "STQL query to select components (mutually exclusive with --type, --tag, --identifier)") - stscobra.MarkMutexFlags(cmd, []string{"type", "stql"}, "query", true) return cmd } @@ -75,12 +68,7 @@ func RunStateCommand( return common.NewExecutionError(fmt.Errorf("limit must be a positive number, got: %d", args.Limit)) } - var query string - if args.STQL != "" { - query = args.STQL - } else { - query = buildSTQLQuery(args.ComponentType, args.Tags, args.Identifiers) - } + query := buildSTQLQuery(args.ComponentType, args.Tags, args.Identifiers) metadata := stackstate_api.NewQueryMetadata( false, diff --git a/cmd/topology/topology_state_test.go b/cmd/topology/topology_state_test.go index 38c149f9..dec8ef46 100644 --- a/cmd/topology/topology_state_test.go +++ b/cmd/topology/topology_state_test.go @@ -220,29 +220,3 @@ func TestTopologyStateLimit(t *testing.T) { table := tableCalls[0] assert.Len(t, table.Data, 2) // Only 2 components due to limit } - -func TestTopologyStateWithSTQL(t *testing.T) { - cli, cmd := setStateCmd(t) - cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponse() - - di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--stql", `type = "custom" AND healthState = "CRITICAL"`) - - calls := *cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotCalls - assert.Len(t, calls, 1) - - // Verify the custom STQL query is used directly - call := calls[0] - request := call.PviewSnapshotRequest - assert.Equal(t, `type = "custom" AND healthState = "CRITICAL"`, request.Query) -} - -func TestTopologyStateSTQLMutuallyExclusiveWithType(t *testing.T) { - cli, cmd := setStateCmd(t) - cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponse() - - _, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--type", "test type", "--stql", "type = \"test\"") - - assert.Error(t, err) - assert.Contains(t, err.Error(), "type") - assert.Contains(t, err.Error(), "stql") -}