diff --git a/cmd/ingestionapikey.go b/cmd/ingestionapikey.go new file mode 100644 index 00000000..c2c531a3 --- /dev/null +++ b/cmd/ingestionapikey.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/stackvista/stackstate-cli/cmd/ingestionapikey" + "github.com/stackvista/stackstate-cli/internal/di" +) + +func IngestionApiKeyCommand(deps *di.Deps) *cobra.Command { + cmd := &cobra.Command{ + Use: "ingestion-api-key", + Short: "Manage Ingestion API Keys", + Long: "Manage API Keys used by ingestion pipelines, means data (spans, metrics, logs an so on) send by STS Agent, OTel and so on.", + } + + cmd.AddCommand(ingestionapikey.CreateCommand(deps)) + cmd.AddCommand(ingestionapikey.ListCommand(deps)) + cmd.AddCommand(ingestionapikey.DeleteCommand(deps)) + return cmd +} diff --git a/cmd/ingestionapikey/ingestionapikey_create.go b/cmd/ingestionapikey/ingestionapikey_create.go new file mode 100644 index 00000000..a37b5e99 --- /dev/null +++ b/cmd/ingestionapikey/ingestionapikey_create.go @@ -0,0 +1,71 @@ +package ingestionapikey + +import ( + "fmt" + "time" + + "github.com/gookit/color" + "github.com/spf13/cobra" + "github.com/stackvista/stackstate-cli/generated/stackstate_api" + "github.com/stackvista/stackstate-cli/internal/common" + "github.com/stackvista/stackstate-cli/internal/di" +) + +const ( + DateFormat = "2006-01-02" +) + +type CreateArgs struct { + Name string + Expiration time.Time + Description string +} + +func CreateCommand(deps *di.Deps) *cobra.Command { + args := &CreateArgs{} + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new Ingestion Api Key", + Long: "Creates a token and then returns it in the response, the token can't be obtained any more after that so store it in the safe space.", + RunE: deps.CmdRunEWithApi(RunIngestionApiKeyGenerationCommand(args)), + } + + common.AddRequiredNameFlagVar(cmd, &args.Name, "Name of the API Key") + cmd.Flags().TimeVar(&args.Expiration, "expiration", time.Time{}, []string{DateFormat}, "Expiration date of the API Key") + cmd.Flags().StringVar(&args.Description, "description", "", "Optional description of the API Key") + return cmd +} + +func RunIngestionApiKeyGenerationCommand(args *CreateArgs) di.CmdWithApiFn { + return func(cmd *cobra.Command, cli *di.Deps, api *stackstate_api.APIClient, serverInfo *stackstate_api.ServerInfo) common.CLIError { + req := stackstate_api.GenerateIngestionApiKeyRequest{ + Name: args.Name, + } + + if len(args.Description) > 0 { + req.Description = &args.Description + } + + if !args.Expiration.IsZero() { + m := args.Expiration.UnixMilli() + req.Expiration = &m + } + + ingestionApiKeyAPI := api.IngestionApiKeyApi.GenerateIngestionApiKey(cli.Context) + + serviceToken, resp, err := ingestionApiKeyAPI.GenerateIngestionApiKeyRequest(req).Execute() + if err != nil { + return common.NewResponseError(err, resp) + } + + if cli.IsJson() { + cli.Printer.PrintJson(map[string]interface{}{ + "ingestion-api-key": serviceToken, + }) + } else { + cli.Printer.Success(fmt.Sprintf("Ingestion API Key generated: %s\n", color.White.Render(serviceToken.ApiKey))) + } + + return nil + } +} diff --git a/cmd/ingestionapikey/ingestionapikey_create_test.go b/cmd/ingestionapikey/ingestionapikey_create_test.go new file mode 100644 index 00000000..2768a15f --- /dev/null +++ b/cmd/ingestionapikey/ingestionapikey_create_test.go @@ -0,0 +1,93 @@ +package ingestionapikey + +import ( + "testing" + + "github.com/stackvista/stackstate-cli/generated/stackstate_api" + "github.com/stackvista/stackstate-cli/internal/di" + "github.com/stretchr/testify/assert" +) + +func TestIngestApiKeyGenerate(t *testing.T) { + cli := di.NewMockDeps(t) + cmd := CreateCommand(&cli.Deps) + + cli.MockClient.ApiMocks.IngestionApiKeyApi.GenerateIngestionApiKeyResponse.Result = stackstate_api.GeneratedIngestionApiKeyResponse{ + Name: "test-token", + ApiKey: "test-token-key", + Expiration: int64p(1590105600000), + Description: stringp("test-token-description"), + } + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--name", "test-token", "--description", "test-token-description", "--expiration", "2020-05-22") + + checkCreateCall(t, cli.MockClient.ApiMocks.IngestionApiKeyApi.GenerateIngestionApiKeyCalls, "test-token", stringp("test-token-description"), int64p(1590105600000)) + assert.Equal(t, []string{"Ingestion API Key generated: \x1b[37mtest-token-key\x1b[0m\n"}, *cli.MockPrinter.SuccessCalls) +} + +func TestIngestApiKeyGenerateOnlyRequriedFlags(t *testing.T) { + cli := di.NewMockDeps(t) + cmd := CreateCommand(&cli.Deps) + + cli.MockClient.ApiMocks.IngestionApiKeyApi.GenerateIngestionApiKeyResponse.Result = stackstate_api.GeneratedIngestionApiKeyResponse{ + Name: "test-token2", + ApiKey: "test-token2-key", + } + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--name", "test-token2") + + checkCreateCall(t, cli.MockClient.ApiMocks.IngestionApiKeyApi.GenerateIngestionApiKeyCalls, "test-token2", nil, nil) + assert.Equal(t, []string{"Ingestion API Key generated: \x1b[37mtest-token2-key\x1b[0m\n"}, *cli.MockPrinter.SuccessCalls) +} + +func TestIngestApiKeyGenerateJSON(t *testing.T) { + cli := di.NewMockDeps(t) + cmd := CreateCommand(&cli.Deps) + + r := &stackstate_api.GeneratedIngestionApiKeyResponse{ + Name: "test-token", + ApiKey: "test-token-key", + Expiration: int64p(1590105600000), + Description: stringp("test-token-description"), + } + + cli.MockClient.ApiMocks.IngestionApiKeyApi.GenerateIngestionApiKeyResponse.Result = *r + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--name", "test-token", "--description", "test-token-description", "--expiration", "2020-05-22", "-o", "json") + + checkCreateCall(t, cli.MockClient.ApiMocks.IngestionApiKeyApi.GenerateIngestionApiKeyCalls, "test-token", stringp("test-token-description"), int64p(1590105600000)) + assert.Equal(t, + []map[string]interface{}{{ + "ingestion-api-key": r, + }}, + *cli.MockPrinter.PrintJsonCalls, + ) + assert.False(t, cli.MockPrinter.HasNonJsonCalls) +} + +func int64p(i int64) *int64 { + return &i +} + +func stringp(i string) *string { + return &i +} + +func checkCreateCall(t *testing.T, calls *[]stackstate_api.GenerateIngestionApiKeyCall, name string, description *string, expiration *int64) { + assert.Len(t, *calls, 1) + + call := (*calls)[0] + assert.Equal(t, name, call.PgenerateIngestionApiKeyRequest.Name) + + if description != nil { + assert.Equal(t, *description, *call.PgenerateIngestionApiKeyRequest.Description) + } else { + assert.Nil(t, call.PgenerateIngestionApiKeyRequest.Description) + } + + if expiration != nil { + assert.Equal(t, *expiration, *call.PgenerateIngestionApiKeyRequest.Expiration) + } else { + assert.Nil(t, call.PgenerateIngestionApiKeyRequest.Expiration) + } +} diff --git a/cmd/ingestionapikey/ingestionapikey_delete.go b/cmd/ingestionapikey/ingestionapikey_delete.go new file mode 100644 index 00000000..f80a16da --- /dev/null +++ b/cmd/ingestionapikey/ingestionapikey_delete.go @@ -0,0 +1,47 @@ +package ingestionapikey + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/stackvista/stackstate-cli/generated/stackstate_api" + "github.com/stackvista/stackstate-cli/internal/common" + "github.com/stackvista/stackstate-cli/internal/di" +) + +type DeleteArgs struct { + ID int64 +} + +func DeleteCommand(deps *di.Deps) *cobra.Command { + args := &DeleteArgs{} + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete an Ingestion Api Key", + Long: "Deleted key can't be used by sources, so all ingestion pipelines for that key will fail.", + RunE: deps.CmdRunEWithApi(RunIngestionApiKeyDeleteCommand(args)), + } + + common.AddRequiredIDFlagVar(cmd, &args.ID, "ID of the Ingestion Api Key to delete") + + return cmd +} + +func RunIngestionApiKeyDeleteCommand(args *DeleteArgs) di.CmdWithApiFn { + return func(cmd *cobra.Command, cli *di.Deps, api *stackstate_api.APIClient, serverInfo *stackstate_api.ServerInfo) common.CLIError { + resp, err := api.IngestionApiKeyApi.DeleteIngestionApiKey(cli.Context, args.ID).Execute() + if err != nil { + return common.NewResponseError(err, resp) + } + + if cli.IsJson() { + cli.Printer.PrintJson(map[string]interface{}{ + "deleted-ingestion-api-key": args.ID, + }) + } else { + cli.Printer.Success(fmt.Sprintf("Ingestion Api Key deleted: %d", args.ID)) + } + + return nil + } +} diff --git a/cmd/ingestionapikey/ingestionapikey_delete_test.go b/cmd/ingestionapikey/ingestionapikey_delete_test.go new file mode 100644 index 00000000..b6573688 --- /dev/null +++ b/cmd/ingestionapikey/ingestionapikey_delete_test.go @@ -0,0 +1,30 @@ +package ingestionapikey + +import ( + "testing" + + "github.com/stackvista/stackstate-cli/internal/di" + "github.com/stretchr/testify/assert" +) + +func TestDeleteShouldFailOnNonIntID(t *testing.T) { + cli := di.NewMockDeps(t) + cmd := DeleteCommand(&cli.Deps) + + _, err := di.ExecuteCommandWithContext(&cli.Deps, cmd, "--id", "foo") + + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "invalid argument \"foo\" for \"-i, --id\"") +} + +func TestDelete(t *testing.T) { + cli := di.NewMockDeps(t) + cmd := DeleteCommand(&cli.Deps) + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd, "--id", "1") + + assert.Len(t, *cli.MockClient.ApiMocks.IngestionApiKeyApi.DeleteIngestionApiKeyCalls, 1) + assert.Equal(t, int64(1), (*cli.MockClient.ApiMocks.IngestionApiKeyApi.DeleteIngestionApiKeyCalls)[0].PingestionApiKeyId) + + assert.Equal(t, []string{"Ingestion Api Key deleted: 1"}, *cli.MockPrinter.SuccessCalls) +} diff --git a/cmd/ingestionapikey/ingestionapikey_list.go b/cmd/ingestionapikey/ingestionapikey_list.go new file mode 100644 index 00000000..11d9670f --- /dev/null +++ b/cmd/ingestionapikey/ingestionapikey_list.go @@ -0,0 +1,58 @@ +package ingestionapikey + +import ( + "sort" + "time" + + "github.com/spf13/cobra" + "github.com/stackvista/stackstate-cli/generated/stackstate_api" + "github.com/stackvista/stackstate-cli/internal/common" + "github.com/stackvista/stackstate-cli/internal/di" + "github.com/stackvista/stackstate-cli/internal/printer" +) + +func ListCommand(deps *di.Deps) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List Ingestion Api Keys", + Long: "Returns only metadata without a key itself.", + RunE: deps.CmdRunEWithApi(RunIngestionApiKeyListCommand), + } + + return cmd +} + +func RunIngestionApiKeyListCommand(cmd *cobra.Command, cli *di.Deps, api *stackstate_api.APIClient, serverInfo *stackstate_api.ServerInfo) common.CLIError { + ingestionApiKeys, resp, err := api.IngestionApiKeyApi.GetIngestionApiKeys(cli.Context).Execute() + if err != nil { + return common.NewResponseError(err, resp) + } + + sort.SliceStable(ingestionApiKeys, func(i, j int) bool { + return ingestionApiKeys[i].Name < ingestionApiKeys[j].Name + }) + + if cli.IsJson() { + cli.Printer.PrintJson(map[string]interface{}{ + "ingestion-api-keys": ingestionApiKeys, + }) + } else { + data := make([][]interface{}, 0) + for _, ingestionApiKey := range ingestionApiKeys { + sid := ingestionApiKey.Id + exp := "" + if ingestionApiKey.Expiration != nil { + exp = time.UnixMilli(*ingestionApiKey.Expiration).Format(DateFormat) + } + data = append(data, []interface{}{sid, ingestionApiKey.Name, exp, ingestionApiKey.Description}) + } + + cli.Printer.Table(printer.TableData{ + Header: []string{"id", "name", "expiration", "description"}, + Data: data, + MissingTableDataMsg: printer.NotFoundMsg{Types: "ingestion api keys"}, + }) + } + + return nil +} diff --git a/cmd/ingestionapikey/ingestionapikey_list_test.go b/cmd/ingestionapikey/ingestionapikey_list_test.go new file mode 100644 index 00000000..33982b7f --- /dev/null +++ b/cmd/ingestionapikey/ingestionapikey_list_test.go @@ -0,0 +1,46 @@ +package ingestionapikey + +import ( + "testing" + + "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 TestIngestionApiKeyList(t *testing.T) { + cli := di.NewMockDeps(t) + cmd := ListCommand(&cli.Deps) + key1desc := "main key" + + cli.MockClient.ApiMocks.IngestionApiKeyApi.GetIngestionApiKeysResponse.Result = []stackstate_api.IngestionApiKey{ + { + Id: 1, + Name: "key1", + Description: &key1desc, + Expiration: int64p(1590105600000), + }, + { + Id: 2, + Name: "key2", + Description: nil, + Expiration: nil, + }, + } + + di.ExecuteCommandWithContextUnsafe(&cli.Deps, cmd) + + tableData := []printer.TableData{ + { + Header: []string{"id", "name", "expiration", "description"}, + Data: [][]interface{}{ + {int64(1), "key1", "2020-05-22", &key1desc}, + {int64(2), "key2", "", (*string)(nil)}, + }, + MissingTableDataMsg: printer.NotFoundMsg{Types: "ingestion api keys"}, + }, + } + + assert.Equal(t, tableData, *cli.MockPrinter.TableCalls) +} diff --git a/cmd/sts.go b/cmd/sts.go index 3798bf49..5db35110 100644 --- a/cmd/sts.go +++ b/cmd/sts.go @@ -28,6 +28,7 @@ func STSCommand(cli *di.Deps) *cobra.Command { cmd.AddCommand(RbacCommand(cli)) cmd.AddCommand(TopicCommand(cli)) cmd.AddCommand(TopologySyncCommand(cli)) + cmd.AddCommand(IngestionApiKeyCommand(cli)) return cmd } diff --git a/internal/di/mock_stackstate_client.go b/internal/di/mock_stackstate_client.go index 55c2656e..7f631c98 100644 --- a/internal/di/mock_stackstate_client.go +++ b/internal/di/mock_stackstate_client.go @@ -33,6 +33,7 @@ type ApiMocks struct { PermissionsApi *stackstate_api.PermissionsApiMock SubjectApi *stackstate_api.SubjectApiMock TopicApi *stackstate_api.TopicApiMock + IngestionApiKeyApi *stackstate_api.IngestionApiKeyApiMock // Admin API: RetentionApi *stackstate_admin_api.RetentionApiMock // MISSING MOCK? You have to manually add new mocks here after generating a new API! @@ -56,6 +57,7 @@ func NewMockStackStateClient() MockStackStateClient { permissionsApi := stackstate_api.NewPermissionsApiMock() subjectApi := stackstate_api.NewSubjectApiMock() topicApi := stackstate_api.NewTopicApiMock() + ingestionApiKeyApi := stackstate_api.NewIngestionApiKeyApiMock() retentionApi := stackstate_admin_api.NewRetentionApiMock() apiMocks := ApiMocks{ @@ -76,6 +78,7 @@ func NewMockStackStateClient() MockStackStateClient { PermissionsApi: &permissionsApi, SubjectApi: &subjectApi, TopicApi: &topicApi, + IngestionApiKeyApi: &ingestionApiKeyApi, RetentionApi: &retentionApi, } @@ -96,6 +99,7 @@ func NewMockStackStateClient() MockStackStateClient { SubscriptionApi: apiMocks.SubscriptionApi, PermissionsApi: apiMocks.PermissionsApi, SubjectApi: apiMocks.SubjectApi, + IngestionApiKeyApi: apiMocks.IngestionApiKeyApi, TopicApi: apiMocks.TopicApi, }