Skip to content

Commit e346031

Browse files
committed
STAC-24049: add crud commands for otel component|relation mappings
1 parent 6d277de commit e346031

File tree

102 files changed

+13770
-760
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+13770
-760
lines changed

cmd/otelcomponentmapping.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ func OtelComponentMappingCommand(deps *di.Deps) *cobra.Command {
1515

1616
cmd.AddCommand(otelcomponentmapping.OtelComponentMappingListCommand(deps))
1717
cmd.AddCommand(otelcomponentmapping.OtelComponentMappingStatusCommand(deps))
18+
cmd.AddCommand(otelcomponentmapping.OtelComponentMappingDescribeCommand(deps))
19+
cmd.AddCommand(otelcomponentmapping.OtelComponentMappingDeleteCommand(deps))
20+
cmd.AddCommand(otelcomponentmapping.OtelComponentMappingEditCommand(deps))
21+
cmd.AddCommand(otelcomponentmapping.OtelComponentMappingApplyCommand(deps))
1822

1923
return cmd
2024
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package otelcomponentmapping
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
11+
"github.com/stackvista/stackstate-cli/internal/common"
12+
"github.com/stackvista/stackstate-cli/internal/di"
13+
"sigs.k8s.io/kustomize/kyaml/yaml"
14+
)
15+
16+
type ApplyArgs struct {
17+
File string
18+
}
19+
20+
func OtelComponentMappingApplyCommand(deps *di.Deps) *cobra.Command {
21+
args := &ApplyArgs{}
22+
cmd := &cobra.Command{
23+
Use: "apply",
24+
Short: "Create or edit an OTel Component Mapping from YAML",
25+
Long: "Create or edit a OTel Component Mapping from YAML file.",
26+
RunE: deps.CmdRunEWithApi(RunApplyComponentMappingCommand(args)),
27+
}
28+
29+
common.AddRequiredFileFlagVar(cmd, &args.File, "Path to a .yaml file with the mapping definition")
30+
31+
return cmd
32+
}
33+
34+
func RunApplyComponentMappingCommand(args *ApplyArgs) di.CmdWithApiFn {
35+
return func(cmd *cobra.Command, cli *di.Deps, api *stackstate_api.APIClient, serverInfo *stackstate_api.ServerInfo) common.CLIError {
36+
fileBytes, err := os.ReadFile(args.File)
37+
if err != nil {
38+
return common.NewReadFileError(err, args.File)
39+
}
40+
41+
ext := strings.ToLower(filepath.Ext(args.File))
42+
if ext != ".yaml" {
43+
return common.NewCLIArgParseError(fmt.Errorf("unsupported file type: %s. Only .yaml files are supported", ext))
44+
}
45+
46+
return applyYAMLOtelComponentMapping(cli, api, fileBytes)
47+
}
48+
}
49+
50+
func applyYAMLOtelComponentMapping(cli *di.Deps, api *stackstate_api.APIClient, fileBytes []byte) common.CLIError {
51+
var mapping stackstate_api.OtelComponentMapping
52+
if err := yaml.Unmarshal(fileBytes, &mapping); err != nil {
53+
return common.NewCLIArgParseError(fmt.Errorf("failed to parse YAML: %v", err))
54+
}
55+
56+
reqObj := stackstate_api.UpsertOtelComponentMappingsRequest{
57+
Identifier: mapping.Identifier,
58+
Name: mapping.Name,
59+
Description: mapping.Description,
60+
Input: mapping.Input,
61+
Output: mapping.Output,
62+
Vars: mapping.Vars,
63+
ExpireAfter: mapping.ExpireAfter,
64+
}
65+
upserted, resp, err := api.OtelMappingApi.UpsertOtelComponentMappings(cli.Context).UpsertOtelComponentMappingsRequest(reqObj).Execute()
66+
if err != nil {
67+
return common.NewResponseError(err, resp)
68+
}
69+
if cli.IsJson() {
70+
cli.Printer.PrintJson(map[string]interface{}{"otel_component_mapping": upserted})
71+
} else {
72+
cli.Printer.Success(fmt.Sprintf("OTel component mapping upserted successfully! Identifier: %s, Name: %s", mapping.GetIdentifier(), mapping.GetName()))
73+
}
74+
return nil
75+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package otelcomponentmapping_test
2+
3+
import (
4+
"errors"
5+
"os"
6+
"testing"
7+
8+
"github.com/stackvista/stackstate-cli/cmd/otelcomponentmapping"
9+
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
10+
"github.com/stackvista/stackstate-cli/internal/di"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
"gopkg.in/yaml.v2"
14+
)
15+
16+
func createTempYamlFile(t *testing.T, content string) string {
17+
t.Helper()
18+
tmpfile, err := os.CreateTemp(os.TempDir(), "test_componentmapping_*.yaml")
19+
if err != nil {
20+
panic(err)
21+
}
22+
23+
_, err = tmpfile.Write([]byte(content))
24+
assert.NoError(t, err)
25+
tmpfile.Close()
26+
return tmpfile.Name()
27+
}
28+
29+
func toOtelMappingItem(m stackstate_api.OtelComponentMapping) stackstate_api.OtelMappingItem {
30+
return stackstate_api.OtelMappingItem{
31+
Name: m.Name,
32+
Identifier: &m.Identifier,
33+
}
34+
}
35+
36+
func TestOtelComponentMappingApply_SuccessYaml(t *testing.T) {
37+
cli := di.NewMockDeps(t)
38+
cmd := otelcomponentmapping.OtelComponentMappingApplyCommand(&cli.Deps)
39+
mapping := stackstate_api.OtelComponentMapping{
40+
Name: "my-mapping",
41+
Identifier: "urn:otel:component:foo",
42+
}
43+
yamlContent, err := yaml.Marshal(mapping)
44+
assert.NoError(t, err)
45+
tmpFile := createTempYamlFile(t, string(yamlContent))
46+
defer os.Remove(tmpFile)
47+
48+
want := toOtelMappingItem(mapping)
49+
cli.MockClient.ApiMocks.OtelMappingApi.UpsertOtelComponentMappingsResponse.Result = want
50+
51+
di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "-f", tmpFile)
52+
53+
assert.Len(t, *cli.MockClient.ApiMocks.OtelMappingApi.UpsertOtelComponentMappingsCalls, 1)
54+
upsertCall := (*cli.MockClient.ApiMocks.OtelMappingApi.UpsertOtelComponentMappingsCalls)[0]
55+
assert.Equal(t, mapping.Identifier, upsertCall.PupsertOtelComponentMappingsRequest.Identifier)
56+
assert.Equal(t, mapping.Name, upsertCall.PupsertOtelComponentMappingsRequest.Name)
57+
}
58+
59+
func TestOtelComponentMappingApply_SuccessJson(t *testing.T) {
60+
cli := di.NewMockDeps(t)
61+
cmd := otelcomponentmapping.OtelComponentMappingApplyCommand(&cli.Deps)
62+
mapping := stackstate_api.OtelComponentMapping{
63+
Name: "my-mapping",
64+
Identifier: "urn:otel:component:foo",
65+
}
66+
yamlContent, err := yaml.Marshal(mapping)
67+
assert.NoError(t, err)
68+
tmpFile := createTempYamlFile(t, string(yamlContent))
69+
defer os.Remove(tmpFile)
70+
71+
want := toOtelMappingItem(mapping)
72+
cli.MockClient.ApiMocks.OtelMappingApi.UpsertOtelComponentMappingsResponse.Result = want
73+
74+
di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "-f", tmpFile, "-o", "json")
75+
76+
assert.Len(t, *cli.MockClient.ApiMocks.OtelMappingApi.UpsertOtelComponentMappingsCalls, 1)
77+
78+
output := *cli.MockPrinter.PrintJsonCalls
79+
assert.Len(t, output, 1)
80+
val, ok := output[0]["otel_component_mapping"]
81+
assert.True(t, ok)
82+
actualPtr, ok := val.(*stackstate_api.OtelMappingItem)
83+
if ok {
84+
assert.Equal(t, &want, actualPtr)
85+
} else {
86+
actualVal, ok := val.(stackstate_api.OtelMappingItem)
87+
require.True(t, ok, "PrintJsonCalls type is not OtelMappingItem or *OtelMappingItem")
88+
assert.Equal(t, want, actualVal)
89+
}
90+
}
91+
92+
func TestOtelComponentMappingApply_FileNotFound(t *testing.T) {
93+
cli := di.NewMockDeps(t)
94+
cmd := otelcomponentmapping.OtelComponentMappingApplyCommand(&cli.Deps)
95+
badPath := "/not/a/real/path.yaml"
96+
require.Panics(t, func() {
97+
di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "-f", badPath)
98+
}, "expected panic for missing file during apply command execution")
99+
}
100+
101+
func TestOtelComponentMappingApply_WrongExtension(t *testing.T) {
102+
cli := di.NewMockDeps(t)
103+
cmd := otelcomponentmapping.OtelComponentMappingApplyCommand(&cli.Deps)
104+
tmpFile := createTempYamlFile(t, "irrelevant-content")
105+
newPath := tmpFile + ".txt"
106+
err := os.Rename(tmpFile, newPath)
107+
assert.NoError(t, err)
108+
defer os.Remove(newPath)
109+
require.Panics(t, func() {
110+
di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "-f", newPath)
111+
}, "expected panic for wrong extension during apply command execution")
112+
}
113+
114+
func TestOtelComponentMappingApply_InvalidYaml(t *testing.T) {
115+
cli := di.NewMockDeps(t)
116+
cmd := otelcomponentmapping.OtelComponentMappingApplyCommand(&cli.Deps)
117+
tmpFile := createTempYamlFile(t, "not: [valid, yaml,,,")
118+
defer os.Remove(tmpFile)
119+
require.Panics(t, func() {
120+
di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "-f", tmpFile)
121+
}, "expected panic for invalid YAML during apply command execution")
122+
}
123+
124+
func TestOtelComponentMappingApply_ApiError(t *testing.T) {
125+
cli := di.NewMockDeps(t)
126+
cmd := otelcomponentmapping.OtelComponentMappingApplyCommand(&cli.Deps)
127+
mapping := stackstate_api.OtelComponentMapping{
128+
Name: "my-mapping",
129+
Identifier: "urn:otel:component:foo",
130+
}
131+
yamlContent, err := yaml.Marshal(mapping)
132+
assert.NoError(t, err)
133+
tmpFile := createTempYamlFile(t, string(yamlContent))
134+
defer os.Remove(tmpFile)
135+
136+
cli.MockClient.ApiMocks.OtelMappingApi.UpsertOtelComponentMappingsResponse.Error = errors.New("api failure")
137+
138+
require.Panics(t, func() {
139+
di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "-f", tmpFile)
140+
}, "expected panic for API error during apply command execution")
141+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package otelcomponentmapping
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
8+
"github.com/stackvista/stackstate-cli/internal/common"
9+
"github.com/stackvista/stackstate-cli/internal/di"
10+
)
11+
12+
type DeleteArgs struct {
13+
Identifier string
14+
}
15+
16+
func OtelComponentMappingDeleteCommand(deps *di.Deps) *cobra.Command {
17+
args := &DeleteArgs{}
18+
cmd := &cobra.Command{
19+
Use: "delete",
20+
Short: "Delete an OTel Component Mapping by identifier (URN)",
21+
Long: "Delete an OTel Component Mapping by identifier (URN)",
22+
RunE: deps.CmdRunEWithApi(RunDeleteComponentMappingCommand(args)),
23+
}
24+
25+
common.AddRequiredIdentifierFlagVar(cmd, &args.Identifier, "Identifier (URN) of the Component Mapping to delete")
26+
27+
return cmd
28+
}
29+
30+
func RunDeleteComponentMappingCommand(args *DeleteArgs) di.CmdWithApiFn {
31+
return func(
32+
cmd *cobra.Command,
33+
cli *di.Deps,
34+
api *stackstate_api.APIClient,
35+
serverInfo *stackstate_api.ServerInfo,
36+
) common.CLIError {
37+
if args.Identifier == "" {
38+
return common.NewCLIArgParseError(fmt.Errorf("--identifier is required"))
39+
}
40+
41+
resp, err := api.OtelMappingApi.DeleteOtelComponentMapping(cli.Context, args.Identifier).Execute()
42+
if err != nil {
43+
return common.NewResponseError(err, resp)
44+
}
45+
46+
result := map[string]interface{}{"deleted_identifier": args.Identifier}
47+
if cli.IsJson() {
48+
cli.Printer.PrintJson(result)
49+
} else {
50+
cli.Printer.Success(fmt.Sprintf("OTel Component Mapping deleted: %s", args.Identifier))
51+
}
52+
53+
return nil
54+
}
55+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package otelcomponentmapping_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stackvista/stackstate-cli/cmd/otelcomponentmapping"
7+
"github.com/stackvista/stackstate-cli/internal/di"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestOtelComponentMappingDelete_Success(t *testing.T) {
12+
cli := di.NewMockDeps(t)
13+
cmd := otelcomponentmapping.OtelComponentMappingDeleteCommand(&cli.Deps)
14+
mockUrn := "urn:otel:component:delete-test"
15+
16+
_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--identifier", mockUrn)
17+
18+
assert.Nil(t, err)
19+
assert.Len(t, *cli.MockPrinter.SuccessCalls, 1)
20+
assert.Contains(t, (*cli.MockPrinter.SuccessCalls)[0], mockUrn)
21+
}
22+
23+
func TestOtelComponentMappingDelete_MissingIdentifier(t *testing.T) {
24+
cli := di.NewMockDeps(t)
25+
cmd := otelcomponentmapping.OtelComponentMappingDeleteCommand(&cli.Deps)
26+
27+
_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd)
28+
29+
assert.NotNil(t, err)
30+
assert.Contains(t, err.Error(), "required flag(s) \"identifier\" not set")
31+
}
32+
33+
func TestOtelComponentMappingDelete_ApiError(t *testing.T) {
34+
cli := di.NewMockDeps(t)
35+
cmd := otelcomponentmapping.OtelComponentMappingDeleteCommand(&cli.Deps)
36+
mockUrn := "urn:otel:component:delete-error"
37+
38+
cli.MockClient.ApiMocks.OtelMappingApi.DeleteOtelComponentMappingResponse.Error = assert.AnError
39+
40+
_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--identifier", mockUrn)
41+
assert.NotNil(t, err)
42+
}
43+
44+
func TestOtelComponentMappingDelete_SuccessJsonOutput(t *testing.T) {
45+
cli := di.NewMockDeps(t)
46+
cmd := otelcomponentmapping.OtelComponentMappingDeleteCommand(&cli.Deps)
47+
mockUrn := "urn:otel:component:delete-test-json"
48+
49+
_, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--identifier", mockUrn, "--output", "json")
50+
51+
assert.Nil(t, err)
52+
expected := []map[string]interface{}{
53+
{"deleted_identifier": mockUrn},
54+
}
55+
assert.Equal(t, expected, *cli.MockPrinter.PrintJsonCalls)
56+
assert.False(t, cli.MockPrinter.HasNonJsonCalls)
57+
}

0 commit comments

Comments
 (0)