feat(cli): add session recordings: read and list cmds

pull/3251/head
Jim 3 years ago committed by Timothy Messier
parent de88a871c6
commit 35a2296092
No known key found for this signature in database
GPG Key ID: EFD2F184F7600572

@ -6,14 +6,16 @@ package sessionrecordings
import (
"time"
"github.com/hashicorp/boundary/api"
)
type ChannelRecording struct {
Id string `json:"id,omitempty"`
BytesUp uint64 `json:"bytes_up,omitempty"`
BytesDown uint64 `json:"bytes_down,omitempty"`
StartTime time.Time `json:"start_time,omitempty"`
EndTime time.Time `json:"end_time,omitempty"`
Duration time.Duration `json:"duration,omitempty"`
MimeTypes []string `json:"mime_types,omitempty"`
Id string `json:"id,omitempty"`
BytesUp uint64 `json:"bytes_up,string,omitempty"`
BytesDown uint64 `json:"bytes_down,string,omitempty"`
StartTime time.Time `json:"start_time,omitempty"`
EndTime time.Time `json:"end_time,omitempty"`
Duration api.Duration `json:"duration,omitempty"`
MimeTypes []string `json:"mime_types,omitempty"`
}

@ -6,16 +6,18 @@ package sessionrecordings
import (
"time"
"github.com/hashicorp/boundary/api"
)
type ConnectionRecording struct {
Id string `json:"id,omitempty"`
ConnectionId string `json:"connection_id,omitempty"`
BytesUp uint64 `json:"bytes_up,omitempty"`
BytesDown uint64 `json:"bytes_down,omitempty"`
BytesUp uint64 `json:"bytes_up,string,omitempty"`
BytesDown uint64 `json:"bytes_down,string,omitempty"`
StartTime time.Time `json:"start_time,omitempty"`
EndTime time.Time `json:"end_time,omitempty"`
Duration time.Duration `json:"duration,omitempty"`
Duration api.Duration `json:"duration,omitempty"`
MimeTypes []string `json:"mime_types,omitempty"`
ChannelRecordings []*ChannelRecording `json:"channel_recordings,omitempty"`
}

@ -5,6 +5,7 @@
package sessionrecordings
import (
"strconv"
"strings"
"github.com/hashicorp/boundary/api"
@ -25,6 +26,7 @@ type options struct {
withAutomaticVersioning bool
withSkipCurlOutput bool
withFilter string
withRecursive bool
}
func getDefaultOptions() options {
@ -48,6 +50,9 @@ func getOpts(opt ...Option) (options, []api.Option) {
if opts.withFilter != "" {
opts.queryMap["filter"] = opts.withFilter
}
if opts.withRecursive {
opts.queryMap["recursive"] = strconv.FormatBool(opts.withRecursive)
}
return opts, apiOpts
}
@ -67,3 +72,11 @@ func WithFilter(filter string) Option {
o.withFilter = strings.TrimSpace(filter)
}
}
// WithRecursive tells the API to use recursion for listing operations on this
// resource
func WithRecursive(recurse bool) Option {
return func(o *options) {
o.withRecursive = true
}
}

@ -19,11 +19,11 @@ type SessionRecording struct {
Scope *scopes.ScopeInfo `json:"scope,omitempty"`
SessionId string `json:"session_id,omitempty"`
StorageBucketId string `json:"storage_bucket_id,omitempty"`
BytesUp uint64 `json:"bytes_up,omitempty"`
BytesDown uint64 `json:"bytes_down,omitempty"`
BytesUp uint64 `json:"bytes_up,string,omitempty"`
BytesDown uint64 `json:"bytes_down,string,omitempty"`
StartTime time.Time `json:"start_time,omitempty"`
EndTime time.Time `json:"end_time,omitempty"`
Duration time.Duration `json:"duration,omitempty"`
Duration api.Duration `json:"duration,omitempty"`
Type string `json:"type,omitempty"`
State string `json:"state,omitempty"`
ErrorDetails string `json:"error_details,omitempty"`

@ -24,6 +24,11 @@ func TestTypes(t *testing.T) {
json: `"12345s"`,
want: Duration{Duration: 12345000000000},
},
{
name: "Duration-valid-with-float",
json: `"1.2345s"`,
want: Duration{Duration: 1234500000},
},
{
name: "Duration-valid",
json: `"1h"`,

@ -74,3 +74,17 @@ All that's left is to incorporate it into Boundary's CLI for the appropriate com
* Run `make cli` and `make install`, before attempting to test cli changes
[test example]: tests/api/authmethods/classification_test.go
## Additional steps for new API/CLI commands:
* After building out the gRPC service, add the new API service definition to the
`inputStructs` for API generation: `internal/api/genapi` then run `make api`
or `make gen`
* Add the new CLI command definition to the `inputStructs` for CLI generation:
`internal/cmd/gencli` then run `make cli` or `make gen`
* Add the new `cli.CommandFactory` to the `cmd.initCommands(...)`:
`internal/cmd` then run `make cli` or `make gen`
* Fill out the required funcs in the command's `func.go` following the same
suggestion to define consts for field names and reuse them everywhere they are
required.
* Run `make cli` and `make install`, before attempting to test cli changes

@ -1008,19 +1008,6 @@ var inputStructs = []*structInfo{
versionEnabled: true,
recursiveListing: true,
},
{
inProto: &session_recordings.SessionRecording{},
outFile: "sessionrecordings/session_recording.gen.go",
templates: []*template.Template{
clientTemplate,
readTemplate,
listTemplate,
},
pluralResourceName: "session-recordings",
createResponseTypes: []string{ReadResponseType, ListResponseType},
recursiveListing: true,
versionEnabled: false,
},
{
inProto: &session_recordings.User{},
outFile: "sessionrecordings/user.gen.go",
@ -1040,10 +1027,47 @@ var inputStructs = []*structInfo{
{
inProto: &session_recordings.ConnectionRecording{},
outFile: "sessionrecordings/connection_recording.gen.go",
fieldOverrides: []fieldInfo{
// int64 fields get marshalled by protobuf as strings, so we have
// to tell the json parser that their json representation is a
// string but they go into Go int64 types.
{Name: "BytesUp", JsonTags: []string{"string"}},
{Name: "BytesDown", JsonTags: []string{"string"}},
},
},
{
inProto: &session_recordings.ChannelRecording{},
outFile: "sessionrecordings/channel_recording.gen.go",
fieldOverrides: []fieldInfo{
// int64 fields get marshalled by protobuf as strings, so we have
// to tell the json parser that their json representation is a
// string but they go into Go int64 types.
{Name: "BytesUp", JsonTags: []string{"string"}},
{Name: "BytesDown", JsonTags: []string{"string"}},
},
},
{
// this must be the last block of session recording blocks, otherwise
// the bits beyond inProto and outFile will get overwritten by
// subsequent session recording blocks
inProto: &session_recordings.SessionRecording{},
outFile: "sessionrecordings/session_recording.gen.go",
templates: []*template.Template{
clientTemplate,
readTemplate,
listTemplate,
},
pluralResourceName: "session-recordings",
createResponseTypes: []string{ReadResponseType, ListResponseType},
recursiveListing: true,
versionEnabled: false,
fieldOverrides: []fieldInfo{
// int64 fields get marshalled by protobuf as strings, so we have
// to tell the json parser that their json representation is a
// string but they go into Go int64 types.
{Name: "BytesUp", JsonTags: []string{"string"}},
{Name: "BytesDown", JsonTags: []string{"string"}},
},
},
{
inProto: &workers.Certificate{},

@ -25,6 +25,7 @@ import (
"github.com/hashicorp/boundary/internal/cmd/commands/rolescmd"
"github.com/hashicorp/boundary/internal/cmd/commands/scopescmd"
"github.com/hashicorp/boundary/internal/cmd/commands/server"
"github.com/hashicorp/boundary/internal/cmd/commands/sessionrecordingscmd"
"github.com/hashicorp/boundary/internal/cmd/commands/sessionscmd"
"github.com/hashicorp/boundary/internal/cmd/commands/storagebucketscmd"
"github.com/hashicorp/boundary/internal/cmd/commands/targetscmd"
@ -1024,6 +1025,24 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
}, nil
},
"session-recordings": func() (cli.Command, error) {
return &sessionrecordingscmd.Command{
Command: base.NewCommand(ui),
}, nil
},
"session-recordings read": func() (cli.Command, error) {
return &sessionrecordingscmd.Command{
Command: base.NewCommand(ui),
Func: "read",
}, nil
},
"session-recordings list": func() (cli.Command, error) {
return &sessionrecordingscmd.Command{
Command: base.NewCommand(ui),
Func: "list",
}, nil
},
"storage-buckets": func() (cli.Command, error) {
return &storagebucketscmd.Command{
Command: base.NewCommand(ui),

@ -718,6 +718,7 @@ func (c *Command) Run(args []string) int {
c.DevLoopbackPluginId = "pl_1234567890"
c.EnabledPlugins = append(c.EnabledPlugins, base.EnabledPluginLoopback)
c.Config.Controller.Scheduler.JobRunIntervalDuration = 100 * time.Millisecond
c.Info["Generated Dev Loopback plugin id"] = c.DevLoopbackPluginId
}
switch c.flagDatabaseUrl {
case "":

@ -0,0 +1,255 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package sessionrecordingscmd
import (
"fmt"
"strings"
"time"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/sessionrecordings"
"github.com/hashicorp/boundary/internal/cmd/base"
)
type extraCmdVars struct{}
func (c *Command) extraHelpFunc(helpMap map[string]func() string) string {
var helpStr string
switch c.Func {
case "":
return base.WrapForHelpText([]string{
"Usage: boundary session-recordings [sub command] [options] [args]",
"",
" This command allows operations on Boundary session recordings.",
"",
" Read a session recording:",
"",
` $ boundary session-recordings read -id s_1234567890`,
"",
" Please see the sessions subcommand help for detailed usage information.",
})
default:
helpStr = helpMap["base"]()
}
return helpStr + c.Flags().Help()
}
func (c *Command) printListTable(items []*sessionrecordings.SessionRecording) string {
if len(items) == 0 {
return "No session recordings found"
}
var output []string
output = []string{
"",
"Session Recording 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 c.FlagRecursive && item.Scope.Id != "" {
output = append(output,
fmt.Sprintf(" Scope ID: %s", item.Scope.Id),
)
}
if item.SessionId != "" {
output = append(output,
fmt.Sprintf(" Session ID: %s", item.SessionId),
)
}
if item.StorageBucketId != "" {
output = append(output,
fmt.Sprintf(" Storage Bucket ID: %s", item.StorageBucketId),
)
}
if !item.StartTime.IsZero() {
output = append(output,
fmt.Sprintf(" Start Time: %s", item.StartTime.Local().Format(time.RFC1123)),
)
}
if !item.EndTime.IsZero() {
output = append(output,
fmt.Sprintf(" End Time: %s", item.EndTime.Local().Format(time.RFC1123)),
)
}
if item.Type != "" {
output = append(output,
fmt.Sprintf(" Type: %s", item.Type),
)
}
if item.State != "" {
output = append(output,
fmt.Sprintf(" State: %s", item.State),
)
}
if len(item.AuthorizedActions) > 0 {
output = append(output,
" Authorized Actions:",
base.WrapSlice(6, item.AuthorizedActions),
)
}
}
return base.WrapForHelpText(output)
}
func printItemTable(item *sessionrecordings.SessionRecording, resp *api.Response) string {
const (
durationKey = "Duration (Seconds)"
)
nonAttributeMap := map[string]any{}
if item.Id != "" {
nonAttributeMap["ID"] = item.Id
}
if item.Scope.Id != "" {
nonAttributeMap["Scope ID"] = item.Scope.Id
}
if item.SessionId != "" {
nonAttributeMap["Session ID"] = item.SessionId
}
if item.StorageBucketId != "" {
nonAttributeMap["Storage Bucket ID"] = item.StorageBucketId
}
if item.BytesUp != 0 {
nonAttributeMap["Bytes Up"] = item.BytesUp
}
if item.BytesDown != 0 {
nonAttributeMap["Bytes Down"] = item.BytesDown
}
if !item.StartTime.IsZero() {
nonAttributeMap["Start Time"] = item.StartTime.Local().Format(time.RFC1123)
}
if !item.EndTime.IsZero() {
nonAttributeMap["Updated Time"] = item.EndTime.Local().Format(time.RFC1123)
}
if item.Duration.Duration != 0 {
nonAttributeMap[durationKey] = item.Duration.Seconds()
}
if item.Type != "" {
nonAttributeMap["Type"] = item.Type
}
if item.State != "" {
nonAttributeMap["State"] = item.State
}
if item.ErrorDetails != "" {
nonAttributeMap["Error Details"] = item.ErrorDetails
}
if len(item.MimeTypes) > 0 {
nonAttributeMap["Mime Types"] = strings.Join(item.MimeTypes, ", ")
}
if item.Endpoint != "" {
nonAttributeMap["Endpoint"] = item.Endpoint
}
maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil)
ret := []string{
"",
"Session Recording 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),
)
}
if len(item.ConnectionRecordings) > 0 {
maxAttrLen := len(durationKey)
ret = append(ret,
"",
" Connections Recordings:",
)
for _, cr := range item.ConnectionRecordings {
cm := map[string]any{
"ID": cr.Id,
"Connection ID": cr.ConnectionId,
}
if cr.BytesUp != 0 {
cm["Bytes Up"] = cr.BytesUp
}
if cr.BytesDown != 0 {
cm["Bytes Down"] = cr.BytesDown
}
if !cr.StartTime.IsZero() {
cm["Start Time"] = cr.StartTime.Local().Format(time.RFC1123)
}
if !cr.EndTime.IsZero() {
cm["End Time"] = cr.EndTime.Local().Format(time.RFC1123)
}
if cr.Duration.Duration != 0 {
cm[durationKey] = cr.Duration.Seconds()
}
if len(cr.MimeTypes) > 0 {
cm["Mime Types"] = strings.Join(cr.MimeTypes, ", ")
}
ret = append(ret,
base.WrapMap(4, maxAttrLen, cm),
"",
)
if len(cr.ChannelRecordings) > 0 {
var channelRecordings []map[string]any
for _, chr := range cr.ChannelRecordings {
chrm := map[string]any{
"ID": chr.Id,
}
if chr.BytesUp != 0 {
chrm["Bytes Up"] = chr.BytesUp
}
if chr.BytesDown != 0 {
chrm["Bytes Down"] = chr.BytesDown
}
if !chr.StartTime.IsZero() {
chrm["Start Time"] = chr.StartTime.Local().Format(time.RFC1123)
}
if !chr.EndTime.IsZero() {
chrm["End Time"] = chr.EndTime.Local().Format(time.RFC1123)
}
if chr.Duration.Duration != 0 {
chrm[durationKey] = chr.Duration.Seconds()
}
if len(chr.MimeTypes) > 0 {
chrm["Mine Types"] = strings.Join(chr.MimeTypes, ", ")
}
channelRecordings = append(channelRecordings, chrm)
}
ret = append(ret,
"",
" Channel Recordings:",
)
for _, cr := range channelRecordings {
ret = append(ret,
base.WrapMap(6, maxAttrLen, cr),
"",
)
}
}
}
}
return base.WrapForHelpText(ret)
}

@ -0,0 +1,283 @@
// Code generated by "make cli"; DO NOT EDIT.
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package sessionrecordingscmd
import (
"errors"
"fmt"
"sync"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/sessionrecordings"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/common"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
func initFlags() {
flagsOnce.Do(func() {
extraFlags := extraActionsFlagsMapFunc()
for k, v := range extraFlags {
flagsMap[k] = append(flagsMap[k], v...)
}
})
}
var (
_ cli.Command = (*Command)(nil)
_ cli.CommandAutocomplete = (*Command)(nil)
)
type Command struct {
*base.Command
Func string
plural string
extraCmdVars
}
func (c *Command) AutocompleteArgs() complete.Predictor {
initFlags()
return complete.PredictAnything
}
func (c *Command) AutocompleteFlags() complete.Flags {
initFlags()
return c.Flags().Completions()
}
func (c *Command) Synopsis() string {
if extra := extraSynopsisFunc(c); extra != "" {
return extra
}
synopsisStr := "session recording"
return common.SynopsisFunc(c.Func, synopsisStr)
}
func (c *Command) Help() string {
initFlags()
var helpStr string
helpMap := common.HelpMap("session recording")
switch c.Func {
case "read":
helpStr = helpMap[c.Func]() + c.Flags().Help()
case "list":
helpStr = helpMap[c.Func]() + c.Flags().Help()
default:
helpStr = c.extraHelpFunc(helpMap)
}
// Keep linter from complaining if we don't actually generate code using it
_ = helpMap
return helpStr
}
var flagsMap = map[string][]string{
"read": {"id"},
"list": {"scope-id", "filter", "recursive"},
}
func (c *Command) Flags() *base.FlagSets {
if len(flagsMap[c.Func]) == 0 {
return c.FlagSet(base.FlagSetNone)
}
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
common.PopulateCommonFlags(c.Command, f, "session recording", flagsMap, c.Func)
extraFlagsFunc(c, set, f)
return set
}
func (c *Command) Run(args []string) int {
initFlags()
switch c.Func {
case "":
return cli.RunResultHelp
case "create":
return cli.RunResultHelp
case "update":
return cli.RunResultHelp
}
c.plural = "session recording"
switch c.Func {
case "list":
c.plural = "session recordings"
}
f := c.Flags()
if err := f.Parse(args); err != nil {
c.PrintCliError(err)
return base.CommandUserError
}
if strutil.StrListContains(flagsMap[c.Func], "id") && c.FlagId == "" {
c.PrintCliError(errors.New("ID is required but not passed in via -id"))
return base.CommandUserError
}
var opts []sessionrecordings.Option
if strutil.StrListContains(flagsMap[c.Func], "scope-id") {
switch c.Func {
case "list":
if c.FlagScopeId == "" {
c.PrintCliError(errors.New("Scope ID must be passed in via -scope-id or BOUNDARY_SCOPE_ID"))
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
}
sessionrecordingsClient := sessionrecordings.NewClient(client)
switch c.FlagRecursive {
case true:
opts = append(opts, sessionrecordings.WithRecursive(true))
}
if c.FlagFilter != "" {
opts = append(opts, sessionrecordings.WithFilter(c.FlagFilter))
}
var version uint32
if ok := extraFlagsHandlingFunc(c, f, &opts); !ok {
return base.CommandUserError
}
var resp *api.Response
var item *sessionrecordings.SessionRecording
var items []*sessionrecordings.SessionRecording
var readResult *sessionrecordings.SessionRecordingReadResult
var listResult *sessionrecordings.SessionRecordingListResult
switch c.Func {
case "read":
readResult, err = sessionrecordingsClient.Read(c.Context, c.FlagId, opts...)
if exitCode := c.checkFuncError(err); exitCode > 0 {
return exitCode
}
resp = readResult.GetResponse()
item = readResult.GetItem()
case "list":
listResult, err = sessionrecordingsClient.List(c.Context, c.FlagScopeId, opts...)
if exitCode := c.checkFuncError(err); exitCode > 0 {
return exitCode
}
resp = listResult.GetResponse()
items = listResult.GetItems()
}
resp, item, items, err = executeExtraActions(c, resp, item, items, err, sessionrecordingsClient, version, opts)
if exitCode := c.checkFuncError(err); exitCode > 0 {
return exitCode
}
output, err := printCustomActionOutput(c)
if err != nil {
c.PrintCliError(err)
return base.CommandUserError
}
if output {
return base.CommandSuccess
}
switch c.Func {
case "list":
switch base.Format(c.UI) {
case "json":
if ok := c.PrintJsonItems(resp); !ok {
return base.CommandCliError
}
case "table":
c.UI.Output(c.printListTable(items))
}
return base.CommandSuccess
}
switch base.Format(c.UI) {
case "table":
c.UI.Output(printItemTable(item, resp))
case "json":
if ok := c.PrintJsonItem(resp); !ok {
return base.CommandCliError
}
}
return base.CommandSuccess
}
func (c *Command) checkFuncError(err error) int {
if err == nil {
return 0
}
if apiErr := api.AsServerError(err); apiErr != nil {
c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural))
return base.CommandApiError
}
c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error()))
return base.CommandCliError
}
var (
flagsOnce = new(sync.Once)
extraActionsFlagsMapFunc = func() map[string][]string { return nil }
extraSynopsisFunc = func(*Command) string { return "" }
extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {}
extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]sessionrecordings.Option) bool { return true }
executeExtraActions = func(_ *Command, inResp *api.Response, inItem *sessionrecordings.SessionRecording, inItems []*sessionrecordings.SessionRecording, inErr error, _ *sessionrecordings.Client, _ uint32, _ []sessionrecordings.Option) (*api.Response, *sessionrecordings.SessionRecording, []*sessionrecordings.SessionRecording, error) {
return inResp, inItem, inItems, inErr
}
printCustomActionOutput = func(*Command) (bool, error) { return false, nil }
)

@ -566,6 +566,17 @@ var inputStructs = map[string][]*cmdInfo{
VersionedActions: []string{"cancel"},
},
},
"sessionrecordings": {
{
ResourceType: resource.SessionRecording.String(),
Pkg: "sessionrecordings",
StdActions: []string{"read", "list"},
Container: "Scope",
HasExtraCommandVars: true,
HasExtraHelpFunc: true,
HasId: true,
},
},
"storagebuckets": {
{
ResourceType: resource.StorageBucket.String(),

Loading…
Cancel
Save