diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index 97fa0f40aa..8bbecdddc6 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -1349,6 +1349,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", } }), + "targets create rdp": wrapper.Wrap(func() wrapper.WrappableCommand { + return &targetscmd.RdpCommand{ + Command: base.NewCommand(ui, opts...), + Func: "create", + } + }), "targets update": wrapper.Wrap(func() wrapper.WrappableCommand { return &targetscmd.Command{ Command: base.NewCommand(ui, opts...), @@ -1361,6 +1367,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "update", } }), + "targets update rdp": wrapper.Wrap(func() wrapper.WrappableCommand { + return &targetscmd.RdpCommand{ + Command: base.NewCommand(ui, opts...), + Func: "update", + } + }), "targets update ssh": wrapper.Wrap(func() wrapper.WrappableCommand { return &targetscmd.SshCommand{ Command: base.NewCommand(ui, opts...), diff --git a/internal/cmd/commands/targetscmd/rdp_funcs.go b/internal/cmd/commands/targetscmd/rdp_funcs.go new file mode 100644 index 0000000000..0efba3753e --- /dev/null +++ b/internal/cmd/commands/targetscmd/rdp_funcs.go @@ -0,0 +1,291 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package targetscmd + +import ( + "fmt" + "strconv" + "time" + + "github.com/hashicorp/boundary/api/targets" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/go-bexpr" +) + +func init() { + extraRdpActionsFlagsMapFunc = extraRdpActionsFlagsMapFuncImpl + extraRdpFlagsFunc = extraRdpFlagsFuncImpl + extraRdpFlagsHandlingFunc = extraRdpFlagsHandlingFuncImpl + extraRdpSynopsisFunc = extraRdpSynopsisFuncImpl +} + +func extraRdpActionsFlagsMapFuncImpl() map[string][]string { + return map[string][]string{ + "create": { + "address", "default-port", "default-client-port", "session-max-seconds", "session-connection-limit", + "egress-worker-filter", "ingress-worker-filter", + "with-alias-value", "with-alias-scope-id", "with-alias-authorize-session-host-id", + }, + "update": { + "address", "default-port", "default-client-port", "session-max-seconds", "session-connection-limit", "egress-worker-filter", "ingress-worker-filter", + }, + } +} + +type extraRdpCmdVars struct { + flagDefaultPort string + flagDefaultClientPort string + flagSessionMaxSeconds string + flagSessionConnectionLimit string + flagEgressWorkerFilter string + flagIngressWorkerFilter string + flagAddress string + flagWithAliasValue string + flagWithAliasScopeId string + flagWithAliasHostId string +} + +func (c *RdpCommand) extraRdpHelpFunc(helpMap map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary targets create rdp [options] [args]", + "", + " Create a rdp-type target. Example:", + "", + ` $ boundary targets create rdp -name prodops -description "Rdp target for ProdOps"`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary targets update rdp [options] [args]", + "", + " Update a rdp-type target given its ID. Example:", + "", + ` $ boundary targets update rdp -id trdp_1234567890 -name "devops" -description "Rdp target for DevOps"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} + +func extraRdpFlagsFuncImpl(c *RdpCommand, set *base.FlagSets, f *base.FlagSet) { + fs := set.NewFlagSet("RDP Target Options") + + for _, name := range flagsRdpMap[c.Func] { + switch name { + case "address": + fs.StringVar(&base.StringVar{ + Name: "address", + Target: &c.flagAddress, + Usage: "Optionally, a valid network address to connect to for this target. Can not be used alongside host sources.", + }) + case "default-port": + fs.StringVar(&base.StringVar{ + Name: "default-port", + Target: &c.flagDefaultPort, + Usage: "Optionally, the default port to set on the target. If not specified, it will be set to 22.", + }) + case "default-client-port": + fs.StringVar(&base.StringVar{ + Name: "default-client-port", + Target: &c.flagDefaultClientPort, + Usage: "The default client port to set on the target.", + }) + case "session-max-seconds": + fs.StringVar(&base.StringVar{ + Name: "session-max-seconds", + Target: &c.flagSessionMaxSeconds, + Usage: `The maximum lifetime of the session, including all connections. Can be specified as an integer number of seconds or a duration string.`, + }) + case "session-connection-limit": + fs.StringVar(&base.StringVar{ + Name: "session-connection-limit", + Target: &c.flagSessionConnectionLimit, + Usage: "The maximum number of connections allowed for a session. -1 means unlimited.", + }) + case "egress-worker-filter": + fs.StringVar(&base.StringVar{ + Name: "egress-worker-filter", + Target: &c.flagEgressWorkerFilter, + Usage: "A boolean expression to filter which egress workers can handle sessions for this target.", + }) + case "ingress-worker-filter": + fs.StringVar(&base.StringVar{ + Name: "ingress-worker-filter", + Target: &c.flagIngressWorkerFilter, + Usage: "A boolean expression to filter which ingress workers can handle sessions for this target.", + }) + case "with-alias-value": + fs.StringVar(&base.StringVar{ + Name: "with-alias-value", + Target: &c.flagWithAliasValue, + Usage: "The value for an alias to be created for and at the same time as this target.", + }) + case "with-alias-scope-id": + fs.StringVar(&base.StringVar{ + Name: "with-alias-scope-id", + Target: &c.flagWithAliasScopeId, + Default: "global", + Usage: "The scope id for an alias to be created for and at the same time as this target.", + }) + case "with-alias-authorize-session-host-id": + fs.StringVar(&base.StringVar{ + Name: "with-alias-authorize-session-host-id", + Target: &c.flagWithAliasHostId, + Usage: "The authorize session host id flag used by an alias to be created for and at the same time as this target.", + }) + } + } +} + +func extraRdpFlagsHandlingFuncImpl(c *RdpCommand, _ *base.FlagSets, opts *[]targets.Option) bool { + switch c.flagDefaultPort { + case "": + case "null": + *opts = append(*opts, targets.DefaultRdpTargetDefaultPort()) + default: + port, err := strconv.ParseUint(c.flagDefaultPort, 10, 32) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing %q: %s", c.flagDefaultPort, err)) + return false + } + *opts = append(*opts, targets.WithRdpTargetDefaultPort(uint32(port))) + } + + switch c.flagDefaultClientPort { + case "": + case "null": + *opts = append(*opts, targets.DefaultRdpTargetDefaultClientPort()) + default: + port, err := strconv.ParseUint(c.flagDefaultClientPort, 10, 32) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing %q: %s", c.flagDefaultClientPort, err)) + return false + } + *opts = append(*opts, targets.WithRdpTargetDefaultClientPort(uint32(port))) + } + + switch c.flagSessionMaxSeconds { + case "": + case "null": + *opts = append(*opts, targets.DefaultSessionMaxSeconds()) + default: + var final uint32 + dur, err := strconv.ParseUint(c.flagSessionMaxSeconds, 10, 32) + if err == nil { + final = uint32(dur) + } else { + dur, err := time.ParseDuration(c.flagSessionMaxSeconds) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing %q: %s", c.flagSessionMaxSeconds, err)) + return false + } + final = uint32(dur.Seconds()) + } + *opts = append(*opts, targets.WithSessionMaxSeconds(final)) + } + + switch c.flagSessionConnectionLimit { + case "": + case "null": + *opts = append(*opts, targets.DefaultSessionConnectionLimit()) + default: + limit, err := strconv.ParseInt(c.flagSessionConnectionLimit, 10, 32) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing %q: %s", c.flagSessionConnectionLimit, err)) + return false + } + *opts = append(*opts, targets.WithSessionConnectionLimit(int32(limit))) + } + switch c.flagEgressWorkerFilter { + case "": + case "null": + *opts = append(*opts, targets.DefaultEgressWorkerFilter()) + default: + if _, err := bexpr.CreateEvaluator(c.flagEgressWorkerFilter); err != nil { + c.UI.Error(fmt.Sprintf("Unable to successfully parse egress filter expression: %s", err)) + return false + } + *opts = append(*opts, targets.WithEgressWorkerFilter(c.flagEgressWorkerFilter)) + } + switch c.flagIngressWorkerFilter { + case "": + case "null": + *opts = append(*opts, targets.DefaultIngressWorkerFilter()) + default: + if _, err := bexpr.CreateEvaluator(c.flagIngressWorkerFilter); err != nil { + c.UI.Error(fmt.Sprintf("Unable to successfully parse ingress filter expression: %s", err)) + return false + } + *opts = append(*opts, targets.WithIngressWorkerFilter(c.flagIngressWorkerFilter)) + } + + switch c.flagAddress { + case "": + case "null": + *opts = append(*opts, targets.DefaultAddress()) + default: + *opts = append(*opts, targets.WithAddress(c.flagAddress)) + } + + var aliasValue string + switch c.flagWithAliasValue { + case "": + case "null": + c.UI.Error("The with-alias-value flag cannot be set to null") + return false + default: + aliasValue = c.flagWithAliasValue + } + + var aliasScopeId string + switch c.flagWithAliasScopeId { + case "": + case "null": + c.UI.Error("The with-alias-scope-id flag cannot be set to null") + return false + default: + aliasScopeId = c.flagWithAliasScopeId + } + + var aliasHostId string + switch c.flagWithAliasHostId { + case "": + case "null": + c.UI.Error("The with-alias-authorize-session-host-id flag cannot be set to null") + return false + default: + aliasHostId = c.flagWithAliasHostId + } + + switch { + case aliasValue != "" && aliasScopeId == "": + c.UI.Error("The with-alias-value flag must be used with the with-alias-scope-id flag") + return false + case aliasValue != "" && aliasScopeId != "": + a := targets.Alias{ + Value: aliasValue, + ScopeId: aliasScopeId, + } + if aliasHostId != "" { + a.Attributes = &targets.TargetAliasAttributes{ + AuthorizeSessionArguments: &targets.AuthorizeSessionArguments{ + HostId: aliasHostId, + }, + } + } + *opts = append(*opts, targets.WithAliases([]targets.Alias{a})) + } + + return true +} + +func extraRdpSynopsisFuncImpl(_ *RdpCommand) string { + return "Create a rdp-type target (HCP only)" +} diff --git a/internal/cmd/commands/targetscmd/rdp_targets.gen.go b/internal/cmd/commands/targetscmd/rdp_targets.gen.go new file mode 100644 index 0000000000..486b6fd080 --- /dev/null +++ b/internal/cmd/commands/targetscmd/rdp_targets.gen.go @@ -0,0 +1,294 @@ +// Code generated by "make cli"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package targetscmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/targets" + "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 initRdpFlags() { + flagsOnce.Do(func() { + extraFlags := extraRdpActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsRdpMap[k] = append(flagsRdpMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*RdpCommand)(nil) + _ cli.CommandAutocomplete = (*RdpCommand)(nil) +) + +type RdpCommand struct { + *base.Command + + Func string + + plural string + + extraRdpCmdVars +} + +func (c *RdpCommand) AutocompleteArgs() complete.Predictor { + initRdpFlags() + return complete.PredictAnything +} + +func (c *RdpCommand) AutocompleteFlags() complete.Flags { + initRdpFlags() + return c.Flags().Completions() +} + +func (c *RdpCommand) Synopsis() string { + if extra := extraRdpSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "target" + + synopsisStr = fmt.Sprintf("%s %s", "rdp-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *RdpCommand) Help() string { + initRdpFlags() + + var helpStr string + helpMap := common.HelpMap("target") + + switch c.Func { + + default: + + helpStr = c.extraRdpHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsRdpMap = map[string][]string{ + + "create": {"scope-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *RdpCommand) Flags() *base.FlagSets { + if len(flagsRdpMap[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, "rdp-type target", flagsRdpMap, c.Func) + + extraRdpFlagsFunc(c, set, f) + + return set +} + +func (c *RdpCommand) Run(args []string) int { + initRdpFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "rdp-type target" + switch c.Func { + case "list": + c.plural = "rdp-type targets" + } + + f := c.Flags() + + var alias string + alias, args = base.ExtractAliasFromArgs(args) + + if alias != "" { + if c.FlagId != "" { + c.PrintCliError(errors.New("Cannot specify both an alias and id; choose one or the other")) + return base.CommandUserError + } + c.FlagId = alias + } + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsRdpMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []targets.Option + + if strutil.StrListContains(flagsRdpMap[c.Func], "scope-id") { + switch c.Func { + + case "create": + 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 + } + targetsClient := targets.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, targets.DefaultName()) + default: + opts = append(opts, targets.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, targets.DefaultDescription()) + default: + opts = append(opts, targets.WithDescription(c.FlagDescription)) + } + + switch c.FlagRecursive { + case true: + opts = append(opts, targets.WithRecursive(true)) + } + + if c.FlagFilter != "" { + opts = append(opts, targets.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, targets.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if ok := extraRdpFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + var item *targets.Target + + var createResult *targets.TargetCreateResult + + var updateResult *targets.TargetUpdateResult + + switch c.Func { + + case "create": + createResult, err = targetsClient.Create(c.Context, "rdp", c.FlagScopeId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = createResult.GetResponse() + item = createResult.GetItem() + + case "update": + updateResult, err = targetsClient.Update(c.Context, c.FlagId, version, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = updateResult.GetResponse() + item = updateResult.GetItem() + + } + + resp, item, err = executeExtraRdpActions(c, resp, item, err, targetsClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomRdpActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + } + + 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 *RdpCommand) 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 ( + extraRdpActionsFlagsMapFunc = func() map[string][]string { return nil } + extraRdpSynopsisFunc = func(*RdpCommand) string { return "" } + extraRdpFlagsFunc = func(*RdpCommand, *base.FlagSets, *base.FlagSet) {} + extraRdpFlagsHandlingFunc = func(*RdpCommand, *base.FlagSets, *[]targets.Option) bool { return true } + executeExtraRdpActions = func(_ *RdpCommand, inResp *api.Response, inItem *targets.Target, inErr error, _ *targets.Client, _ uint32, _ []targets.Option) (*api.Response, *targets.Target, error) { + return inResp, inItem, inErr + } + printCustomRdpActionOutput = func(*RdpCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index 8aa5a610ce..b6174b96ca 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -742,6 +742,24 @@ var inputStructs = map[string][]*cmdInfo{ AliasFieldFlag: "FlagId", FlagNameOverwrittenByAlias: "id", }, + { + ResourceType: resource.Target.String(), + Pkg: "targets", + StdActions: []string{"create", "update"}, + SubActionPrefix: "rdp", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + Container: "Scope", + HasDescription: true, + VersionedActions: []string{"update"}, + NeedsSubtypeInCreate: true, + UsesAlias: true, + AliasFieldFlag: "FlagId", + FlagNameOverwrittenByAlias: "id", + }, }, "users": { { diff --git a/internal/tests/cli/boundary/target.bats b/internal/tests/cli/boundary/target.bats index 0f59409d9e..4f8e953ae6 100644 --- a/internal/tests/cli/boundary/target.bats +++ b/internal/tests/cli/boundary/target.bats @@ -150,3 +150,8 @@ load _target_host_sources run update_address $id "localhost" [ "$status" -eq 0 ] } + +teardown_file() { + local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME_WITH_ADDR) + run delete_target $id +}