454 lines
13 KiB
Go
454 lines
13 KiB
Go
// Copyright IBM Corp. 2024, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package sessionrecordingscmd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/api"
|
|
"github.com/hashicorp/boundary/api/sessionrecordings"
|
|
"github.com/hashicorp/boundary/internal/cmd/base"
|
|
"github.com/mitchellh/go-wordwrap"
|
|
"github.com/posener/complete"
|
|
)
|
|
|
|
// ExportCommandAction defines actions supported by a session recording export
|
|
// resource.
|
|
type ExportCommandAction string
|
|
|
|
const (
|
|
// ExportCommandActionCreate defines the export creation action.
|
|
ExportCommandActionCreate ExportCommandAction = "create"
|
|
// ExportCommandActionRead defines the export read action.
|
|
ExportCommandActionRead ExportCommandAction = "read"
|
|
// ExportCommandActionList defines the export listing action.
|
|
ExportCommandActionList ExportCommandAction = "list"
|
|
// ExportCommandActionCancel defines the export cancel action.
|
|
ExportCommandActionCancel ExportCommandAction = "cancel"
|
|
)
|
|
|
|
// ExportCommand defines a session recording export command. This command
|
|
// exposes all actions that are available for session recording exports.
|
|
// Note: Custom code has to be used as this sub-resource does not fit well with
|
|
// the existing cli autogen logic.
|
|
type ExportCommand struct {
|
|
*base.Command
|
|
Action ExportCommandAction
|
|
|
|
flagMimeType string
|
|
}
|
|
|
|
func (c *ExportCommand) Synopsis() string {
|
|
switch c.Action {
|
|
case ExportCommandActionCreate:
|
|
// Create refers to starting, reading or cancelling because the "export"
|
|
// sub-command is both used to start an export and also as an
|
|
// entry-point for the "export read" and "export cancel" sub-commands.
|
|
return wordwrap.WrapString("Start, read or cancel a session recording export", base.TermWidth)
|
|
case ExportCommandActionRead:
|
|
return wordwrap.WrapString("Read a session recording export", base.TermWidth)
|
|
case ExportCommandActionCancel:
|
|
return wordwrap.WrapString("Cancel a session recording export", base.TermWidth)
|
|
case ExportCommandActionList:
|
|
return wordwrap.WrapString("List session recording exports", base.TermWidth)
|
|
default:
|
|
return wordwrap.WrapString("Perform actions on session recording export resources", base.TermWidth)
|
|
}
|
|
}
|
|
|
|
func (c *ExportCommand) Help() string {
|
|
var out []string
|
|
|
|
switch c.Action {
|
|
case ExportCommandActionCreate:
|
|
out = append(out,
|
|
"Usage: boundary session-recordings export [action] [args]",
|
|
"",
|
|
" Create a new export. Example:",
|
|
"",
|
|
` $ boundary session-recordings export -id cr_1234567890 -mime-type video/webm`,
|
|
"",
|
|
" Also used as an entrypoint to read or cancel an existing export.",
|
|
"",
|
|
" Please see subcommand help for detailed usage information.",
|
|
)
|
|
case ExportCommandActionRead:
|
|
out = append(out,
|
|
"Usage: boundary session-recordings export read [args]",
|
|
"",
|
|
" Read an existing export. Example:",
|
|
"",
|
|
` $ boundary session-recordings export read -id exp_1234567890`,
|
|
)
|
|
case ExportCommandActionCancel:
|
|
out = append(out,
|
|
"Usage: boundary session-recordings export cancel [args]",
|
|
"",
|
|
" Cancel an existing export. Example:",
|
|
"",
|
|
` $ boundary session-recordings export cancel -id exp_1234567890`,
|
|
)
|
|
case ExportCommandActionList:
|
|
out = append(out,
|
|
"Usage: boundary session-recordings list-exports [args]",
|
|
"",
|
|
" List exports within an enclosing scope or resource. Example:",
|
|
"",
|
|
` $ boundary session-recordings list-exports -scope-id global -recursive true`,
|
|
)
|
|
}
|
|
out = append(out, "", "")
|
|
|
|
return base.WrapForHelpText(out) + c.Flags().Help()
|
|
}
|
|
|
|
func (c *ExportCommand) Flags() *base.FlagSets {
|
|
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
|
|
|
|
fs := set.NewFlagSet("Command Options")
|
|
switch c.Action {
|
|
case ExportCommandActionCreate:
|
|
fs.StringVar(&base.StringVar{
|
|
Name: "connection-recording-id",
|
|
Target: &c.FlagId,
|
|
Usage: "The id of the connection recording to export",
|
|
Aliases: []string{"cr-id"},
|
|
})
|
|
fs.StringVar(&base.StringVar{
|
|
Name: "mime-type",
|
|
Target: &c.flagMimeType,
|
|
Usage: "The export's requested mime-type. Must be a valid Boundary API mime-type",
|
|
Aliases: []string{"mt"},
|
|
})
|
|
case ExportCommandActionRead, ExportCommandActionCancel:
|
|
fs.StringVar(&base.StringVar{
|
|
Name: "id",
|
|
Target: &c.FlagId,
|
|
Usage: "The export's id",
|
|
})
|
|
case ExportCommandActionList:
|
|
fs.StringVar(&base.StringVar{
|
|
Name: "scope-id",
|
|
Target: &c.FlagScopeId,
|
|
Usage: "The scope in which to make the request.",
|
|
EnvVar: "BOUNDARY_SCOPE_ID",
|
|
Default: "global",
|
|
})
|
|
fs.BoolVar(&base.BoolVar{
|
|
Name: "recursive",
|
|
Target: &c.FlagRecursive,
|
|
Usage: "If set, the list operation will be applied recursively into child scopes, if supported by the type.",
|
|
Default: false,
|
|
})
|
|
}
|
|
|
|
return set
|
|
}
|
|
|
|
func (c *ExportCommand) AutocompleteArgs() complete.Predictor {
|
|
return complete.PredictAnything
|
|
}
|
|
|
|
func (c *ExportCommand) AutocompleteFlags() complete.Flags {
|
|
return c.Flags().Completions()
|
|
}
|
|
|
|
func (c *ExportCommand) Run(args []string) int {
|
|
fs := c.Flags()
|
|
err := fs.Parse(args)
|
|
if err != nil {
|
|
c.PrintCliError(err)
|
|
return base.CommandUserError
|
|
}
|
|
|
|
client, err := c.Client()
|
|
if c.WrapperCleanupFunc != nil {
|
|
defer func() {
|
|
if err := c.WrapperCleanupFunc(); err != nil {
|
|
c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err))
|
|
}
|
|
}()
|
|
}
|
|
if err != nil {
|
|
c.PrintCliError(fmt.Errorf("Error creating API client: %w", err))
|
|
return base.CommandCliError
|
|
}
|
|
|
|
srCl := sessionrecordings.NewClient(client)
|
|
switch c.Action {
|
|
case ExportCommandActionCreate:
|
|
return c.create(srCl)
|
|
case ExportCommandActionRead:
|
|
return c.read(srCl)
|
|
case ExportCommandActionList:
|
|
return c.list(srCl)
|
|
case ExportCommandActionCancel:
|
|
return c.cancel(srCl)
|
|
}
|
|
|
|
return base.CommandSuccess
|
|
}
|
|
|
|
func (c *ExportCommand) create(cl *sessionrecordings.Client) int {
|
|
switch {
|
|
case c.FlagId == "":
|
|
c.PrintCliError(errors.New("Missing connection recording id. Must be provided via -connection-recording-id"))
|
|
return base.CommandUserError
|
|
case c.flagMimeType == "":
|
|
c.PrintCliError(fmt.Errorf("Missing mime-type. Must be provided via -mime-type"))
|
|
return base.CommandUserError
|
|
}
|
|
|
|
result, err := cl.Export(c.Context, c.FlagId, c.flagMimeType)
|
|
if err != nil {
|
|
if apiErr := api.AsServerError(err); apiErr != nil {
|
|
c.PrintApiError(apiErr, "Error from controller when starting a session connection recording export")
|
|
return base.CommandApiError
|
|
}
|
|
c.PrintCliError(fmt.Errorf("Session connection recording export error: %w", err))
|
|
return base.CommandCliError
|
|
}
|
|
|
|
switch base.Format(c.UI) {
|
|
case "table":
|
|
c.UI.Output(c.printItemTable(result.GetItem(), result.GetResponse()))
|
|
case "json":
|
|
if ok := c.PrintJsonItem(result.GetResponse()); !ok {
|
|
return base.CommandCliError
|
|
}
|
|
}
|
|
|
|
return base.CommandSuccess
|
|
}
|
|
|
|
func (c *ExportCommand) read(cl *sessionrecordings.Client) int {
|
|
switch { //nolint:staticcheck // QF1002. For consistency with how we check function inputs.
|
|
case c.FlagId == "":
|
|
c.PrintCliError(fmt.Errorf("Missing export id. Must be provided via -id"))
|
|
return base.CommandUserError
|
|
}
|
|
|
|
result, err := cl.ReadExport(c.Context, c.FlagId)
|
|
if err != nil {
|
|
if apiErr := api.AsServerError(err); apiErr != nil {
|
|
c.PrintApiError(apiErr, "Error from controller when reading a session connection recording export")
|
|
return base.CommandApiError
|
|
}
|
|
c.PrintCliError(fmt.Errorf("Session connection recording export read error: %w", err))
|
|
return base.CommandCliError
|
|
}
|
|
|
|
switch base.Format(c.UI) {
|
|
case "table":
|
|
c.UI.Output(c.printItemTable(result.GetItem(), result.GetResponse()))
|
|
case "json":
|
|
if ok := c.PrintJsonItem(result.GetResponse()); !ok {
|
|
return base.CommandCliError
|
|
}
|
|
}
|
|
|
|
return base.CommandSuccess
|
|
}
|
|
|
|
func (c *ExportCommand) cancel(cl *sessionrecordings.Client) int {
|
|
switch { //nolint:staticcheck // QF1002. For consistency with how we check function inputs.
|
|
case c.FlagId == "":
|
|
c.PrintCliError(fmt.Errorf("Missing export id. Must be provided via -id"))
|
|
return base.CommandUserError
|
|
}
|
|
|
|
result, err := cl.CancelExport(c.Context, c.FlagId)
|
|
if err != nil {
|
|
if apiErr := api.AsServerError(err); apiErr != nil {
|
|
c.PrintApiError(apiErr, "Error from controller when cancelling a session connection recording export")
|
|
return base.CommandApiError
|
|
}
|
|
c.PrintCliError(fmt.Errorf("Session connection recording export cancel error: %w", err))
|
|
return base.CommandCliError
|
|
}
|
|
|
|
switch base.Format(c.UI) {
|
|
case "table":
|
|
c.UI.Output("The cancel operation completed successfully.")
|
|
case "json":
|
|
if ok := c.PrintJsonItem(result.GetResponse()); !ok {
|
|
return base.CommandCliError
|
|
}
|
|
}
|
|
|
|
return base.CommandSuccess
|
|
}
|
|
|
|
func (c *ExportCommand) list(cl *sessionrecordings.Client) int {
|
|
switch { //nolint:staticcheck // QF1002. For consistency with how we check function inputs.
|
|
case c.FlagScopeId == "":
|
|
c.PrintCliError(fmt.Errorf("Missing scope id. Must be provided via -scope-id"))
|
|
return base.CommandUserError
|
|
}
|
|
|
|
opts := make([]sessionrecordings.Option, 0, 1)
|
|
if c.FlagRecursive {
|
|
opts = append(opts, sessionrecordings.WithRecursive(true))
|
|
}
|
|
|
|
result, err := cl.ListExports(c.Context, c.FlagScopeId, opts...)
|
|
if err != nil {
|
|
if apiErr := api.AsServerError(err); apiErr != nil {
|
|
c.PrintApiError(apiErr, "Error from controller when listing session connection recording exports")
|
|
return base.CommandApiError
|
|
}
|
|
c.PrintCliError(fmt.Errorf("Session connection recording export listing error: %w", err))
|
|
return base.CommandCliError
|
|
}
|
|
|
|
switch base.Format(c.UI) {
|
|
case "table":
|
|
c.UI.Output(c.printListTable(result.GetItems()))
|
|
case "json":
|
|
if ok := c.PrintJsonItems(result.GetResponse()); !ok {
|
|
return base.CommandCliError
|
|
}
|
|
}
|
|
|
|
return base.CommandSuccess
|
|
}
|
|
|
|
func (c *ExportCommand) printItemTable(item *sessionrecordings.Export, resp *api.Response) string {
|
|
nonAttributeMap := map[string]any{}
|
|
if item.Id != "" {
|
|
nonAttributeMap["ID"] = item.Id
|
|
}
|
|
if item.Scope.Id != "" {
|
|
nonAttributeMap["Scope ID"] = item.Scope.Id
|
|
}
|
|
if item.ConnectionRecordingId != "" {
|
|
nonAttributeMap["Connection Recording ID"] = item.ConnectionRecordingId
|
|
}
|
|
if item.MimeType != "" {
|
|
nonAttributeMap["Mime Type"] = item.MimeType
|
|
}
|
|
if item.State != "" {
|
|
nonAttributeMap["State"] = item.State
|
|
}
|
|
if item.ProgressPercent > 0 {
|
|
nonAttributeMap["Progress (%)"] = item.ProgressPercent
|
|
}
|
|
if item.WorkerId != "" {
|
|
nonAttributeMap["Worker ID"] = item.WorkerId
|
|
}
|
|
if item.Error != "" {
|
|
nonAttributeMap["Error"] = item.Error
|
|
}
|
|
if !item.CreatedTime.IsZero() {
|
|
nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123)
|
|
}
|
|
if !item.UpdatedTime.IsZero() {
|
|
nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123)
|
|
}
|
|
|
|
maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil)
|
|
ret := []string{
|
|
"",
|
|
"Export information:",
|
|
base.WrapMap(2, maxLength+2, nonAttributeMap),
|
|
}
|
|
if item.Scope != nil {
|
|
ret = append(ret,
|
|
"",
|
|
" Scope:",
|
|
base.ScopeInfoForOutput(item.Scope, maxLength),
|
|
)
|
|
}
|
|
if len(item.AuthorizedActions) > 0 {
|
|
ret = append(ret,
|
|
"",
|
|
" Authorized Actions:",
|
|
base.WrapSlice(4, item.AuthorizedActions),
|
|
)
|
|
}
|
|
|
|
return base.WrapForHelpText(ret)
|
|
}
|
|
|
|
func (c *ExportCommand) printListTable(items []*sessionrecordings.Export) string {
|
|
if len(items) == 0 {
|
|
return "No exports found"
|
|
}
|
|
|
|
output := []string{
|
|
"",
|
|
"Export information:",
|
|
}
|
|
|
|
for i, item := range items {
|
|
if i > 0 {
|
|
output = append(output, "")
|
|
}
|
|
if item.Id != "" {
|
|
output = append(output,
|
|
fmt.Sprintf(" ID: %s", item.Id),
|
|
)
|
|
} else {
|
|
output = append(output,
|
|
fmt.Sprintf(" ID: %s", "(not available)"),
|
|
)
|
|
}
|
|
if item.ConnectionRecordingId != "" {
|
|
output = append(output,
|
|
fmt.Sprintf(" Connection Recording ID: %s", item.ConnectionRecordingId),
|
|
)
|
|
}
|
|
if c.FlagRecursive && item.Scope.Id != "" {
|
|
output = append(output,
|
|
fmt.Sprintf(" Scope ID: %s", item.Scope.Id),
|
|
)
|
|
}
|
|
if !item.CreatedTime.IsZero() {
|
|
output = append(output,
|
|
fmt.Sprintf(" Created Time: %s", item.CreatedTime.Local().Format(time.RFC1123)),
|
|
)
|
|
}
|
|
if !item.UpdatedTime.IsZero() {
|
|
output = append(output,
|
|
fmt.Sprintf(" Updated Time: %s", item.UpdatedTime.Local().Format(time.RFC1123)),
|
|
)
|
|
}
|
|
if item.MimeType != "" {
|
|
output = append(output,
|
|
fmt.Sprintf(" Mime Type: %s", item.MimeType),
|
|
)
|
|
}
|
|
if item.State != "" {
|
|
output = append(output,
|
|
fmt.Sprintf(" State: %s", item.State),
|
|
)
|
|
}
|
|
if item.ProgressPercent > 0 {
|
|
output = append(output,
|
|
fmt.Sprintf(" Progress (%%): %d", item.ProgressPercent),
|
|
)
|
|
}
|
|
if item.WorkerId != "" {
|
|
output = append(output,
|
|
fmt.Sprintf(" Worker ID: %s", item.WorkerId),
|
|
)
|
|
}
|
|
if item.Error != "" {
|
|
output = append(output,
|
|
fmt.Sprintf(" Error: %s", item.Error),
|
|
)
|
|
}
|
|
if len(item.AuthorizedActions) > 0 {
|
|
output = append(output,
|
|
" Authorized Actions:",
|
|
base.WrapSlice(6, item.AuthorizedActions),
|
|
)
|
|
}
|
|
}
|
|
|
|
return base.WrapForHelpText(output)
|
|
}
|