From 2b0ff59810933d8ae8d26e5dd614e3576b594cfa Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Mon, 14 Jul 2025 18:32:49 -0600 Subject: [PATCH 1/3] CDI-258: Add License Subcommand - Update github workflow to support DevOps User and Key - Add License Subcommand, Logic, and Options - Add License testing and internal testing - Update existing tests with new Options - Update to Go 1.24.5 and Update Dependencies - Update OptionType to be int, and enumerate via 'iota' - Add LicenseProduct and LicenseVersion custom types --- .../workflows/code-analysis-lint-test.yaml | 2 + .golangci.yml | 1 - cmd/config/list_keys_test.go | 2 + cmd/license/license.go | 63 +++++++ cmd/license/license_test.go | 87 +++++++++ cmd/root.go | 2 + examples/plugin/plugin.go | 8 +- go.mod | 18 +- go.sum | 32 ++-- internal/commands/config/set_internal.go | 46 +++-- internal/commands/license/license_internal.go | 97 ++++++++++ .../commands/license/license_internal_test.go | 127 +++++++++++++ internal/configuration/config/add_profile.go | 6 +- .../configuration/config/delete_profile.go | 2 +- .../configuration/config/list_keys_yaml.go | 2 +- internal/configuration/configuration.go | 3 + internal/configuration/license/license.go | 116 ++++++++++++ internal/configuration/options/options.go | 172 ++++++++++-------- .../configuration/options/options_test.go | 4 +- internal/configuration/platform/export.go | 12 +- internal/configuration/plugin/add.go | 2 +- internal/configuration/profiles/profiles.go | 2 +- internal/configuration/request/request.go | 16 +- internal/configuration/root/root.go | 14 +- .../configuration/services/pingfederate.go | 26 +-- internal/configuration/services/pingone.go | 10 +- internal/customtypes/license_product.go | 80 ++++++++ internal/customtypes/license_version.go | 46 +++++ internal/plugins/plugins.go | 2 + internal/profiles/validate.go | 54 ++++-- .../testing/testutils_koanf/koanf_utils.go | 5 + 31 files changed, 873 insertions(+), 186 deletions(-) create mode 100644 cmd/license/license.go create mode 100644 cmd/license/license_test.go create mode 100644 internal/commands/license/license_internal.go create mode 100644 internal/commands/license/license_internal_test.go create mode 100644 internal/configuration/license/license.go create mode 100644 internal/customtypes/license_product.go create mode 100644 internal/customtypes/license_version.go diff --git a/.github/workflows/code-analysis-lint-test.yaml b/.github/workflows/code-analysis-lint-test.yaml index cee6e38f..3e22a569 100644 --- a/.github/workflows/code-analysis-lint-test.yaml +++ b/.github/workflows/code-analysis-lint-test.yaml @@ -132,6 +132,8 @@ jobs: TEST_PINGONE_WORKER_CLIENT_ID: ${{ secrets.TEST_PINGONE_WORKER_CLIENT_ID }} TEST_PINGONE_WORKER_CLIENT_SECRET: ${{ secrets.TEST_PINGONE_WORKER_CLIENT_SECRET }} TEST_PINGONE_REGION_CODE: ${{ secrets.TEST_PINGONE_REGION_CODE }} + TEST_PINGCLI_DEVOPS_USER: ${{ secrets.TEST_PINGCLI_DEVOPS_USER }} + TEST_PINGCLI_DEVOPS_KEY: ${{ secrets.TEST_PINGCLI_DEVOPS_KEY steps: - uses: actions/checkout@v4 diff --git a/.golangci.yml b/.golangci.yml index d659ccb4..59e5a390 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,7 +7,6 @@ linters: - canonicalheader - copyloopvar - decorder - - dogsled - dupword - durationcheck - errcheck diff --git a/cmd/config/list_keys_test.go b/cmd/config/list_keys_test.go index c117b756..450be55d 100644 --- a/cmd/config/list_keys_test.go +++ b/cmd/config/list_keys_test.go @@ -57,6 +57,8 @@ func Example_listKeysValue() { // - export.pingOne.environmentID // - export.serviceGroup // - export.services + // - license.devopsKey + // - license.devopsUser // - noColor // - outputFormat // - plugins diff --git a/cmd/license/license.go b/cmd/license/license.go new file mode 100644 index 00000000..7e09bedc --- /dev/null +++ b/cmd/license/license.go @@ -0,0 +1,63 @@ +// Copyright © 2025 Ping Identity Corporation + +package license + +import ( + "fmt" + + "github.com/pingidentity/pingcli/cmd/common" + license_internal "github.com/pingidentity/pingcli/internal/commands/license" + "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/logger" + "github.com/pingidentity/pingcli/internal/output" + "github.com/spf13/cobra" +) + +const ( + licenseCommandExamples = ` Request a new evaluation license for PingFederate 12.0. + pingcli license request --product pingfederate --version 12.0 + + Request a new evaluation license for PingAccess 6.3. + pingcli license request --product pingaccess --version 6.3` +) + +func NewLicenseCommand() *cobra.Command { + cmd := &cobra.Command{ + Args: common.ExactArgs(0), + DisableFlagsInUseLine: true, // We write our own flags in @Use attribute + Example: licenseCommandExamples, + Long: `Request a new evaluation license for a specific product and version. + +The new license request will be sent to the Ping Identity license server.`, + RunE: licenseRunE, + Short: "Request a new evaluation license.", + Use: "license [flags]", + } + + cmd.Flags().AddFlag(options.LicenseProductOption.Flag) + cmd.Flags().AddFlag(options.LicenseVersionOption.Flag) + cmd.Flags().AddFlag(options.LicenseDevopsUserOption.Flag) + cmd.Flags().AddFlag(options.LicenseDevopsKeyOption.Flag) + + err := cmd.MarkFlagRequired(options.LicenseProductOption.CobraParamName) + if err != nil { + output.SystemError(fmt.Sprintf("Failed to mark flag '%s' as required: %v", options.LicenseProductOption.CobraParamName, err), nil) + } + err = cmd.MarkFlagRequired(options.LicenseVersionOption.CobraParamName) + if err != nil { + output.SystemError(fmt.Sprintf("Failed to mark flag '%s' as required: %v", options.LicenseVersionOption.CobraParamName, err), nil) + } + + return cmd +} + +func licenseRunE(cmd *cobra.Command, args []string) error { + l := logger.Get() + l.Debug().Msgf("License Subcommand Called.") + + if err := license_internal.RunInternalLicense(); err != nil { + return err + } + + return nil +} diff --git a/cmd/license/license_test.go b/cmd/license/license_test.go new file mode 100644 index 00000000..53acb633 --- /dev/null +++ b/cmd/license/license_test.go @@ -0,0 +1,87 @@ +// Copyright © 2025 Ping Identity Corporation + +package license_test + +import ( + "testing" + + "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" +) + +// Test License Command Executes without issue (with all required flags) +func TestLicenseCmd_Execute(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, "license", + "--"+options.LicenseProductOption.CobraParamName, customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, + "--"+options.LicenseVersionOption.CobraParamName, "12.0") + testutils.CheckExpectedError(t, err, nil) +} + +// Test License Command fails when provided too many arguments +func TestLicenseCmd_TooManyArgs(t *testing.T) { + expectedErrorPattern := `^failed to execute 'pingcli license': command accepts 0 arg\(s\), received 1$` + err := testutils_cobra.ExecutePingcli(t, "license", "extra-arg") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test License Command help flag +func TestLicenseCmd_HelpFlag(t *testing.T) { + err := testutils_cobra.ExecutePingcli(t, "license", "--help") + testutils.CheckExpectedError(t, err, nil) + + err = testutils_cobra.ExecutePingcli(t, "license", "-h") + testutils.CheckExpectedError(t, err, nil) +} + +// Test License Command fails with invalid flag +func TestLicenseCmd_InvalidFlag(t *testing.T) { + expectedErrorPattern := `^unknown flag: --invalid$` + err := testutils_cobra.ExecutePingcli(t, "license", "--invalid") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test License Command fails when required product flag is missing +func TestLicenseCmd_MissingProductFlag(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + expectedErrorPattern := `^required flag\(s\) "product" not set$` + err := testutils_cobra.ExecutePingcli(t, "license", + "--"+options.LicenseVersionOption.CobraParamName, "12.0") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test License Command fails when required version flag is missing +func TestLicenseCmd_MissingVersionFlag(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + expectedErrorPattern := `^required flag\(s\) "version" not set$` + err := testutils_cobra.ExecutePingcli(t, "license", + "--"+options.LicenseProductOption.CobraParamName, "pingfederate") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test License Command with shorthand flags +func TestLicenseCmd_ShorthandFlags(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, "license", + "-"+options.LicenseProductOption.Flag.Shorthand, "pingfederate", + "-"+options.LicenseVersionOption.Flag.Shorthand, "12.0") + testutils.CheckExpectedError(t, err, nil) +} + +// Test License Command with a profile +func TestLicenseCmd_Profile(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + err := testutils_cobra.ExecutePingcli(t, "license", + "--"+options.LicenseProductOption.CobraParamName, "pingfederate", + "--"+options.LicenseVersionOption.CobraParamName, "12.0", + "--"+options.RootProfileOption.CobraParamName, "default") + testutils.CheckExpectedError(t, err, nil) +} diff --git a/cmd/root.go b/cmd/root.go index 1a60207b..fa7502ef 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,6 +12,7 @@ import ( "github.com/pingidentity/pingcli/cmd/completion" "github.com/pingidentity/pingcli/cmd/config" "github.com/pingidentity/pingcli/cmd/feedback" + "github.com/pingidentity/pingcli/cmd/license" "github.com/pingidentity/pingcli/cmd/platform" "github.com/pingidentity/pingcli/cmd/plugin" "github.com/pingidentity/pingcli/cmd/request" @@ -51,6 +52,7 @@ func NewRootCommand(version string, commit string) *cobra.Command { platform.NewPlatformCommand(), plugin.NewPluginCommand(), request.NewRequestCommand(), + license.NewLicenseCommand(), ) err := plugins.AddAllPluginToCmd(cmd) diff --git a/examples/plugin/plugin.go b/examples/plugin/plugin.go index 1f8cf927..482111d7 100644 --- a/examples/plugin/plugin.go +++ b/examples/plugin/plugin.go @@ -6,9 +6,11 @@ // command that can be dynamically loaded and executed by the main pingcli // application. This includes implementing the PingCliCommand interface and // serving it over gRPC using Hashicorp's `go-plugin“ library. -package plugin +package main import ( + "fmt" + "github.com/hashicorp/go-plugin" "github.com/pingidentity/pingcli/shared/grpc" ) @@ -68,7 +70,7 @@ func (c *PingCliCommand) Configuration() (*grpc.PingCliCommandConfiguration, err // messages back to the host process, ensuring that all output is displayed // consistently through the main pingcli interface. func (c *PingCliCommand) Run(args []string, logger grpc.Logger) error { - err := logger.Message("Message from plugin", nil) + err := logger.Message(fmt.Sprintf("Args to plugin: %v", args), nil) if err != nil { return err } @@ -93,7 +95,7 @@ func (c *PingCliCommand) Run(args []string, logger grpc.Logger) error { // launches this plugin, this function starts a gRPC server that serves the // PingCliCommand implementation. The go-plugin library handles the handshake // and communication between the host and the plugin process. -func main() { //nolint:unused +func main() { plugin.Serve(&plugin.ServeConfig{ // HandshakeConfig is a shared configuration used to verify that the host // and plugin are compatible. diff --git a/go.mod b/go.mod index 13edd643..1de23040 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/pingidentity/pingcli -go 1.24.4 +go 1.24.5 tool ( github.com/golangci/golangci-lint/v2/cmd/golangci-lint @@ -15,7 +15,7 @@ require ( github.com/knadh/koanf/parsers/yaml v1.1.0 github.com/knadh/koanf/providers/confmap v1.0.0 github.com/knadh/koanf/providers/file v1.2.0 - github.com/knadh/koanf/v2 v2.2.1 + github.com/knadh/koanf/v2 v2.2.2 github.com/manifoldco/promptui v0.9.0 github.com/patrickcping/pingone-go-sdk-v2 v0.13.0 github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.1 @@ -26,7 +26,7 @@ require ( github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 - golang.org/x/mod v0.25.0 + golang.org/x/mod v0.26.0 google.golang.org/grpc v1.73.0 google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 @@ -94,7 +94,7 @@ require ( github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/go-viper/mapstructure/v2 v2.3.0 // indirect github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gofrs/flock v0.12.1 // indirect @@ -224,12 +224,12 @@ require ( go.uber.org/zap v1.24.0 // indirect go.yaml.in/yaml/v3 v3.0.3 // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect - golang.org/x/net v0.39.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect - golang.org/x/tools v0.32.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect honnef.co/go/tools v0.6.1 // indirect diff --git a/go.sum b/go.sum index 17e41296..880a8bed 100644 --- a/go.sum +++ b/go.sum @@ -217,8 +217,8 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= +github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -379,8 +379,8 @@ github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5z github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A= github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U= github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= -github.com/knadh/koanf/v2 v2.2.1 h1:jaleChtw85y3UdBnI0wCqcg1sj1gPoz6D3caGNHtrNE= -github.com/knadh/koanf/v2 v2.2.1/go.mod h1:PSFru3ufQgTsI7IF+95rf9s8XA1+aHxKuO/W+dPoHEY= +github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A= +github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -759,8 +759,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -801,8 +801,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -826,8 +826,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -885,8 +885,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -907,8 +907,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -968,8 +968,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/commands/config/set_internal.go b/internal/commands/config/set_internal.go index a6e08889..7a1bd464 100644 --- a/internal/commands/config/set_internal.go +++ b/internal/commands/config/set_internal.go @@ -115,7 +115,7 @@ func parseKeyValuePair(kvPair string) (string, string, error) { func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options.OptionType) (err error) { switch valueType { - case options.ENUM_BOOL: + case options.BOOL: b := new(customtypes.Bool) if err = b.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a boolean. Allowed [true, false]: %w", vKey, err) @@ -124,7 +124,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_EXPORT_FORMAT: + case options.EXPORT_FORMAT: exportFormat := new(customtypes.ExportFormat) if err = exportFormat.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a valid export format. Allowed [%s]: %w", vKey, strings.Join(customtypes.ExportFormatValidValues(), ", "), err) @@ -133,7 +133,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_EXPORT_SERVICE_GROUP: + case options.EXPORT_SERVICE_GROUP: exportServiceGroup := new(customtypes.ExportServiceGroup) if err = exportServiceGroup.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be valid export service group. Allowed [%s]: %w", vKey, strings.Join(customtypes.ExportServiceGroupValidValues(), ", "), err) @@ -142,7 +142,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_EXPORT_SERVICES: + case options.EXPORT_SERVICES: exportServices := new(customtypes.ExportServices) if err = exportServices.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be valid export service(s). Allowed [%s]: %w", vKey, strings.Join(customtypes.ExportServicesValidValues(), ", "), err) @@ -151,7 +151,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_OUTPUT_FORMAT: + case options.OUTPUT_FORMAT: outputFormat := new(customtypes.OutputFormat) if err = outputFormat.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a valid output format. Allowed [%s]: %w", vKey, strings.Join(customtypes.OutputFormatValidValues(), ", "), err) @@ -160,7 +160,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_PINGONE_REGION_CODE: + case options.PINGONE_REGION_CODE: region := new(customtypes.PingOneRegionCode) if err = region.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a valid PingOne Region Code. Allowed [%s]: %w", vKey, strings.Join(customtypes.PingOneRegionCodeValidValues(), ", "), err) @@ -169,7 +169,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_STRING: + case options.STRING: str := new(customtypes.String) if err = str.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a string: %w", vKey, err) @@ -178,7 +178,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_STRING_SLICE: + case options.STRING_SLICE: strSlice := new(customtypes.StringSlice) if err = strSlice.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a string slice: %w", vKey, err) @@ -187,7 +187,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_UUID: + case options.UUID: uuid := new(customtypes.UUID) if err = uuid.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a valid UUID: %w", vKey, err) @@ -196,7 +196,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_PINGONE_AUTH_TYPE: + case options.PINGONE_AUTH_TYPE: authType := new(customtypes.PingOneAuthenticationType) if err = authType.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a valid PingOne Authentication Type. Allowed [%s]: %w", vKey, strings.Join(customtypes.PingOneAuthenticationTypeValidValues(), ", "), err) @@ -205,7 +205,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_PINGFEDERATE_AUTH_TYPE: + case options.PINGFEDERATE_AUTH_TYPE: authType := new(customtypes.PingFederateAuthenticationType) if err = authType.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a valid PingFederate Authentication Type. Allowed [%s]: %w", vKey, strings.Join(customtypes.PingFederateAuthenticationTypeValidValues(), ", "), err) @@ -214,7 +214,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_INT: + case options.INT: intValue := new(customtypes.Int) if err = intValue.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be an integer: %w", vKey, err) @@ -223,7 +223,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_REQUEST_HTTP_METHOD: + case options.REQUEST_HTTP_METHOD: httpMethod := new(customtypes.HTTPMethod) if err = httpMethod.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a valid HTTP method. Allowed [%s]: %w", vKey, strings.Join(customtypes.HTTPMethodValidValues(), ", "), err) @@ -232,7 +232,7 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } - case options.ENUM_REQUEST_SERVICE: + case options.REQUEST_SERVICE: service := new(customtypes.RequestService) if err = service.Set(vValue); err != nil { return fmt.Errorf("value for key '%s' must be a valid request service. Allowed [%s]: %w", vKey, strings.Join(customtypes.RequestServiceValidValues(), ", "), err) @@ -241,6 +241,24 @@ func setValue(profileKoanf *koanf.Koanf, vKey, vValue string, valueType options. if err != nil { return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) } + case options.LICENSE_PRODUCT: + licenseProduct := new(customtypes.LicenseProduct) + if err = licenseProduct.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a valid license product. Allowed [%s]: %w", vKey, strings.Join(customtypes.LicenseProductValidValues(), ", "), err) + } + err = profileKoanf.Set(vKey, licenseProduct) + if err != nil { + return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + } + case options.LICENSE_VERSION: + licenseVersion := new(customtypes.LicenseVersion) + if err = licenseVersion.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a valid license version. Must be of the form 'major.minor': %w", vKey, err) + } + err = profileKoanf.Set(vKey, licenseVersion) + if err != nil { + return fmt.Errorf("unable to set key '%w' in koanf profile: ", err) + } default: return fmt.Errorf("failed to set configuration: variable type for key '%s' is not recognized", vKey) } diff --git a/internal/commands/license/license_internal.go b/internal/commands/license/license_internal.go new file mode 100644 index 00000000..69582ada --- /dev/null +++ b/internal/commands/license/license_internal.go @@ -0,0 +1,97 @@ +package license_internal + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + + "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/output" + "github.com/pingidentity/pingcli/internal/profiles" +) + +func RunInternalLicense() (err error) { + product, version, devopsUser, devopsKey, err := readLicenseOptionValues() + if err != nil { + return fmt.Errorf("failed to run license request: %w", err) + } + + ctx := context.Background() + licenseData, err := runLicenseRequest(ctx, product, version, devopsUser, devopsKey) + if err != nil { + return fmt.Errorf("failed to run license request: %w", err) + } + + if licenseData == "" { + return fmt.Errorf("failed to run license request: returned license data is empty, please check your request parameters") + } + + output.Message(licenseData, nil) + + return nil +} + +func readLicenseOptionValues() (product, version, devopsUser, devopsKey string, err error) { + product, err = profiles.GetOptionValue(options.LicenseProductOption) + if err != nil { + return "", "", "", "", fmt.Errorf("failed to get product option: %w", err) + } + + version, err = profiles.GetOptionValue(options.LicenseVersionOption) + if err != nil { + return "", "", "", "", fmt.Errorf("failed to get version option: %w", err) + } + + devopsUser, err = profiles.GetOptionValue(options.LicenseDevopsUserOption) + if err != nil { + return "", "", "", "", fmt.Errorf("failed to get devops user option: %w", err) + } + + devopsKey, err = profiles.GetOptionValue(options.LicenseDevopsKeyOption) + if err != nil { + return "", "", "", "", fmt.Errorf("failed to get devops key option: %w", err) + } + + if product == "" || version == "" || devopsUser == "" || devopsKey == "" { + return "", "", "", "", fmt.Errorf("product, version, devops user, and devops key must be specified for license request") + } + + return product, version, devopsUser, devopsKey, nil +} + +func runLicenseRequest(ctx context.Context, product, version, devopsUser, devopsKey string) (licenseData string, err error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://license.pingidentity.com/devops/license", nil) + if err != nil { + return "", fmt.Errorf("failed to create license request: %w", err) + } + + req.Header.Set("Devops-User", devopsUser) + req.Header.Set("Devops-Key", devopsKey) + req.Header.Set("Devops-App", "PingCLI") + req.Header.Set("Devops-Purpose", "download-license") + req.Header.Set("Product", product) + req.Header.Set("Version", version) + + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("failed to execute license request: %w", err) + } + defer func() { + cErr := res.Body.Close() + err = errors.Join(err, cErr) + }() + + body, err := io.ReadAll(res.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + + if res.StatusCode < 200 || res.StatusCode >= 300 { + return "", fmt.Errorf("license request failed with status %d: %s", res.StatusCode, string(body)) + } + + return string(body), nil +} diff --git a/internal/commands/license/license_internal_test.go b/internal/commands/license/license_internal_test.go new file mode 100644 index 00000000..6fd138e1 --- /dev/null +++ b/internal/commands/license/license_internal_test.go @@ -0,0 +1,127 @@ +package license_internal + +import ( + "os" + "testing" + + "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_koanf" +) + +// setLicenseProductAndVersion sets up product and version options +func setLicenseProductAndVersion(product, version string) { + if product != "" { + productVal := customtypes.LicenseProduct(product) + options.LicenseProductOption.CobraParamValue = &productVal + options.LicenseProductOption.Flag.Changed = true + } + + if version != "" { + versionVal := customtypes.LicenseVersion(version) + options.LicenseVersionOption.CobraParamValue = &versionVal + options.LicenseVersionOption.Flag.Changed = true + } +} + +// Test RunInternalLicense function with valid options +func Test_RunInternalLicense_Success(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + setLicenseProductAndVersion(customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, "13.0") + + err := RunInternalLicense() + testutils.CheckExpectedError(t, err, nil) +} + +// Test RunInternalLicense with missing product option +func Test_RunInternalLicense_MissingProduct(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + // Set up test data with missing product + setLicenseProductAndVersion("", "13.0") + + // Run the function + expectedErrorPattern := `^failed to run license request: product, version, devops user, and devops key must be specified for license request$` + err := RunInternalLicense() + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalLicense with missing version option +func Test_RunInternalLicense_MissingVersion(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + setLicenseProductAndVersion(customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, "") + + // Run the function + expectedErrorPattern := `^failed to run license request: product, version, devops user, and devops key must be specified for license request$` + err := RunInternalLicense() + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test readLicenseOptionValues function +func Test_readLicenseOptionValues(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + // Set up test data with all options + setLicenseProductAndVersion(customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, "13.0") + + // Run the function + product, version, devopsUser, devopsKey, err := readLicenseOptionValues() + + testutils.CheckExpectedError(t, err, nil) + if product != customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE { + t.Errorf("expected product %q, got %q", customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, product) + } + if version != "13.0" { + t.Errorf("expected version %q, got %q", "13.0", version) + } + if devopsUser == "" { + t.Error("expected devops user to be set, but it was empty") + } + if devopsKey == "" { + t.Error("expected devops key to be set, but it was empty") + } +} + +func Test_readLicenseOptionValues_EmptyValues(t *testing.T) { + testutils_koanf.InitKoanfs(t) + + setLicenseProductAndVersion(customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, "") + + expectedErrorPattern := `^product, version, devops user, and devops key must be specified for license request$` + _, _, _, _, err := readLicenseOptionValues() + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test runLicenseRequest function success +func Test_runLicenseRequest_Success(t *testing.T) { + licenseData, err := runLicenseRequest( + t.Context(), + customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, + "13.0", + os.Getenv("TEST_PINGCLI_DEVOPS_USER"), + os.Getenv("TEST_PINGCLI_DEVOPS_KEY")) + + testutils.CheckExpectedError(t, err, nil) + if licenseData == "" { + t.Error("expected license data to be non-empty, but it was empty") + } +} + +// Test runLicenseRequest with an invalid devops key +func Test_runLicenseRequest_InvalidDevopsKey(t *testing.T) { + licenseData, err := runLicenseRequest( + t.Context(), + customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, + "13.0", + os.Getenv("TEST_PINGCLI_DEVOPS_USER"), + "invalid-key") + + expectedErrorPattern := `^license request failed with status 401\: \{ "error"\: "Invalid devops-key header" \}$` + testutils.CheckExpectedError(t, err, &expectedErrorPattern) + if licenseData != "" { + t.Error("expected license data to be empty, but it was not") + } +} diff --git a/internal/configuration/config/add_profile.go b/internal/configuration/config/add_profile.go index 0e25318e..430f481d 100644 --- a/internal/configuration/config/add_profile.go +++ b/internal/configuration/config/add_profile.go @@ -31,7 +31,7 @@ func initAddProfileDescriptionOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "", // No koanf key } } @@ -53,7 +53,7 @@ func initAddProfileNameOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "", // No koanf key } } @@ -77,7 +77,7 @@ func initAddProfileSetActiveOption() { NoOptDefVal: "true", // Make this flag a boolean flag }, Sensitive: false, - Type: options.ENUM_BOOL, + Type: options.BOOL, KoanfKey: "", // No koanf key } } diff --git a/internal/configuration/config/delete_profile.go b/internal/configuration/config/delete_profile.go index ec659aee..7afd5d84 100644 --- a/internal/configuration/config/delete_profile.go +++ b/internal/configuration/config/delete_profile.go @@ -31,7 +31,7 @@ func initDeleteAutoAcceptOption() { NoOptDefVal: "true", // Make the flag a boolean flag }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "", // No koanf key } } diff --git a/internal/configuration/config/list_keys_yaml.go b/internal/configuration/config/list_keys_yaml.go index 4260d39b..884d430f 100644 --- a/internal/configuration/config/list_keys_yaml.go +++ b/internal/configuration/config/list_keys_yaml.go @@ -31,7 +31,7 @@ func initConfigListKeysYAMLOption() { NoOptDefVal: "true", // Make this flag a boolean flag }, Sensitive: false, - Type: options.ENUM_BOOL, + Type: options.BOOL, KoanfKey: "", // No koanf key } } diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 9a75f1ec..d388c12e 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -8,6 +8,7 @@ import ( "strings" configuration_config "github.com/pingidentity/pingcli/internal/configuration/config" + configuration_license "github.com/pingidentity/pingcli/internal/configuration/license" "github.com/pingidentity/pingcli/internal/configuration/options" configuration_platform "github.com/pingidentity/pingcli/internal/configuration/platform" configuration_plugin "github.com/pingidentity/pingcli/internal/configuration/plugin" @@ -101,4 +102,6 @@ func InitAllOptions() { configuration_services.InitPingFederateServiceOptions() configuration_services.InitPingOneServiceOptions() + + configuration_license.InitLicenseOptions() } diff --git a/internal/configuration/license/license.go b/internal/configuration/license/license.go new file mode 100644 index 00000000..dacaea9b --- /dev/null +++ b/internal/configuration/license/license.go @@ -0,0 +1,116 @@ +package configuration_license + +import ( + "fmt" + "strings" + + "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitLicenseOptions() { + initProductOption() + initVersionOption() + initDevopsUserOption() + initDevopsKeyOption() +} + +func initProductOption() { + cobraParamName := "product" + cobraValue := new(customtypes.LicenseProduct) + defaultValue := customtypes.LicenseProduct("") + + options.LicenseProductOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "p", + Usage: fmt.Sprintf( + "The product for which to request a license. "+ + "\nOptions are: %s."+ + "\nExample: '%s'", + strings.Join(customtypes.LicenseProductValidValues(), ", "), + customtypes.ENUM_LICENSE_PRODUCT_PING_FEDERATE, + ), + Value: cobraValue, + }, + Sensitive: false, + Type: options.LICENSE_PRODUCT, + KoanfKey: "", // No koanf key + } +} + +func initVersionOption() { + cobraParamName := "version" + cobraValue := new(customtypes.LicenseVersion) + defaultValue := customtypes.LicenseVersion("") + + options.LicenseVersionOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "v", + Usage: "The version of the product for which to request a license. Must be of the form 'major.minor'. " + + "\nExample: '12.3'", + Value: cobraValue, + }, + Sensitive: false, + Type: options.LICENSE_VERSION, + KoanfKey: "", // No koanf key + } +} + +func initDevopsUserOption() { + cobraParamName := "devops-user" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.LicenseDevopsUserOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "PINGCLI_LICENSE_DEVOPS_USER", + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "u", + Usage: "The DevOps user for the license request. " + + "\n See https://developer.pingidentity.com/devops/how-to/devopsRegistration.html on how to register a DevOps user. " + + "\n You can save the DevOps user and key in your profile using the 'pingcli config' commands.", + Value: cobraValue, + }, + Sensitive: false, + Type: options.STRING, + KoanfKey: "license.devopsUser", + } +} + +func initDevopsKeyOption() { + cobraParamName := "devops-key" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.LicenseDevopsKeyOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "PINGCLI_LICENSE_DEVOPS_KEY", + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "k", + Usage: "The DevOps key for the license request. " + + "\n See https://developer.pingidentity.com/devops/how-to/devopsRegistration.html on how to register a DevOps user. " + + "\n You can save the DevOps user and key in your profile using the 'pingcli config' commands.", + Value: cobraValue, + }, + Sensitive: true, + Type: options.STRING, + KoanfKey: "license.devopsKey", + } +} diff --git a/internal/configuration/options/options.go b/internal/configuration/options/options.go index 02a2b6cb..e181c1da 100644 --- a/internal/configuration/options/options.go +++ b/internal/configuration/options/options.go @@ -9,25 +9,27 @@ import ( "github.com/spf13/pflag" ) -type OptionType string +type OptionType int // OptionType enums const ( - ENUM_BOOL OptionType = "ENUM_BOOL" - ENUM_EXPORT_FORMAT OptionType = "ENUM_EXPORT_FORMAT" - ENUM_HEADER OptionType = "ENUM_HEADER" - ENUM_INT OptionType = "ENUM_INT" - ENUM_EXPORT_SERVICE_GROUP OptionType = "ENUM_EXPORT_SERVICE_GROUP" - ENUM_EXPORT_SERVICES OptionType = "ENUM_EXPORT_SERVICES" - ENUM_OUTPUT_FORMAT OptionType = "ENUM_OUTPUT_FORMAT" - ENUM_PINGFEDERATE_AUTH_TYPE OptionType = "ENUM_PINGFEDERATE_AUTH_TYPE" - ENUM_PINGONE_AUTH_TYPE OptionType = "ENUM_PINGONE_AUTH_TYPE" - ENUM_PINGONE_REGION_CODE OptionType = "ENUM_PINGONE_REGION_CODE" - ENUM_REQUEST_HTTP_METHOD OptionType = "ENUM_REQUEST_HTTP_METHOD" - ENUM_REQUEST_SERVICE OptionType = "ENUM_REQUEST_SERVICE" - ENUM_STRING OptionType = "ENUM_STRING" - ENUM_STRING_SLICE OptionType = "ENUM_STRING_SLICE" - ENUM_UUID OptionType = "ENUM_UUID" + BOOL OptionType = iota + EXPORT_FORMAT + EXPORT_SERVICE_GROUP + EXPORT_SERVICES + HEADER + INT + LICENSE_PRODUCT + LICENSE_VERSION + OUTPUT_FORMAT + PINGFEDERATE_AUTH_TYPE + PINGONE_AUTH_TYPE + PINGONE_REGION_CODE + REQUEST_HTTP_METHOD + REQUEST_SERVICE + STRING + STRING_SLICE + UUID ) type Option struct { @@ -43,6 +45,32 @@ type Option struct { func Options() []Option { optList := []Option{ + ConfigAddProfileDescriptionOption, + ConfigAddProfileNameOption, + ConfigAddProfileSetActiveOption, + ConfigDeleteAutoAcceptOption, + ConfigListKeysYamlOption, + ConfigUnmaskSecretValueOption, + + LicenseProductOption, + LicenseVersionOption, + LicenseDevopsUserOption, + LicenseDevopsKeyOption, + + PingFederateAccessTokenAuthAccessTokenOption, + PingFederateAdminAPIPathOption, + PingFederateAuthenticationTypeOption, + PingFederateBasicAuthPasswordOption, + PingFederateBasicAuthUsernameOption, + PingFederateCACertificatePemFilesOption, + PingFederateClientCredentialsAuthClientIDOption, + PingFederateClientCredentialsAuthClientSecretOption, + PingFederateClientCredentialsAuthTokenURLOption, + PingFederateClientCredentialsAuthScopesOption, + PingFederateHTTPSHostOption, + PingFederateInsecureTrustAllTLSOption, + PingFederateXBypassExternalValidationHeaderOption, + PingOneAuthenticationTypeOption, PingOneAuthenticationWorkerClientIDOption, PingOneAuthenticationWorkerClientSecretOption, @@ -50,52 +78,31 @@ func Options() []Option { PingOneRegionCodeOption, PlatformExportExportFormatOption, - PlatformExportServiceGroupOption, - PlatformExportServiceOption, PlatformExportOutputDirectoryOption, PlatformExportOverwriteOption, PlatformExportPingOneEnvironmentIDOption, + PlatformExportServiceGroupOption, + PlatformExportServiceOption, PluginExecutablesOption, - PingFederateHTTPSHostOption, - PingFederateAdminAPIPathOption, - PingFederateXBypassExternalValidationHeaderOption, - PingFederateCACertificatePemFilesOption, - PingFederateInsecureTrustAllTLSOption, - PingFederateBasicAuthUsernameOption, - PingFederateBasicAuthPasswordOption, - PingFederateAccessTokenAuthAccessTokenOption, - PingFederateClientCredentialsAuthClientIDOption, - PingFederateClientCredentialsAuthClientSecretOption, - PingFederateClientCredentialsAuthTokenURLOption, - PingFederateClientCredentialsAuthScopesOption, - PingFederateAuthenticationTypeOption, - - RootActiveProfileOption, - RootProfileOption, - RootColorOption, - RootConfigOption, - RootDetailedExitCodeOption, - RootOutputFormatOption, - ProfileDescriptionOption, - ConfigAddProfileDescriptionOption, - ConfigAddProfileNameOption, - ConfigAddProfileSetActiveOption, - ConfigDeleteAutoAcceptOption, - ConfigListKeysYamlOption, - ConfigUnmaskSecretValueOption, - + RequestAccessTokenExpiryOption, + RequestAccessTokenOption, RequestDataOption, RequestDataRawOption, + RequestFailOption, RequestHeaderOption, RequestHTTPMethodOption, RequestServiceOption, - RequestAccessTokenOption, - RequestAccessTokenExpiryOption, - RequestFailOption, + + RootActiveProfileOption, + RootColorOption, + RootConfigOption, + RootDetailedExitCodeOption, + RootOutputFormatOption, + RootProfileOption, } // Sort the options list by koanf key @@ -106,53 +113,58 @@ func Options() []Option { return optList } -// pingone service options +// 'pingcli config' command options var ( - PingOneAuthenticationTypeOption Option - PingOneAuthenticationWorkerClientIDOption Option - PingOneAuthenticationWorkerClientSecretOption Option - PingOneAuthenticationWorkerEnvironmentIDOption Option - PingOneRegionCodeOption Option + ConfigAddProfileDescriptionOption Option + ConfigAddProfileNameOption Option + ConfigAddProfileSetActiveOption Option + ConfigDeleteAutoAcceptOption Option + ConfigListKeysYamlOption Option + ConfigUnmaskSecretValueOption Option +) + +// License options +var ( + LicenseProductOption Option + LicenseVersionOption Option + LicenseDevopsUserOption Option + LicenseDevopsKeyOption Option ) // pingfederate service options var ( - PingFederateHTTPSHostOption Option + PingFederateAccessTokenAuthAccessTokenOption Option PingFederateAdminAPIPathOption Option - PingFederateXBypassExternalValidationHeaderOption Option - PingFederateCACertificatePemFilesOption Option - PingFederateInsecureTrustAllTLSOption Option - PingFederateBasicAuthUsernameOption Option + PingFederateAuthenticationTypeOption Option PingFederateBasicAuthPasswordOption Option - PingFederateAccessTokenAuthAccessTokenOption Option + PingFederateBasicAuthUsernameOption Option + PingFederateCACertificatePemFilesOption Option PingFederateClientCredentialsAuthClientIDOption Option PingFederateClientCredentialsAuthClientSecretOption Option - PingFederateClientCredentialsAuthTokenURLOption Option PingFederateClientCredentialsAuthScopesOption Option - PingFederateAuthenticationTypeOption Option + PingFederateClientCredentialsAuthTokenURLOption Option + PingFederateHTTPSHostOption Option + PingFederateInsecureTrustAllTLSOption Option + PingFederateXBypassExternalValidationHeaderOption Option ) -// 'pingcli config' command options +// pingone service options var ( - ConfigAddProfileDescriptionOption Option - ConfigAddProfileNameOption Option - ConfigAddProfileSetActiveOption Option - - ConfigListKeysYamlOption Option - - ConfigDeleteAutoAcceptOption Option - - ConfigUnmaskSecretValueOption Option + PingOneAuthenticationTypeOption Option + PingOneAuthenticationWorkerClientIDOption Option + PingOneAuthenticationWorkerClientSecretOption Option + PingOneAuthenticationWorkerEnvironmentIDOption Option + PingOneRegionCodeOption Option ) // 'pingcli platform export' command options var ( PlatformExportExportFormatOption Option - PlatformExportServiceOption Option - PlatformExportServiceGroupOption Option PlatformExportOutputDirectoryOption Option PlatformExportOverwriteOption Option PlatformExportPingOneEnvironmentIDOption Option + PlatformExportServiceGroupOption Option + PlatformExportServiceOption Option ) // 'pingcli plugin' command options @@ -168,21 +180,21 @@ var ( // Root Command Options var ( RootActiveProfileOption Option - RootDetailedExitCodeOption Option - RootProfileOption Option RootColorOption Option RootConfigOption Option + RootDetailedExitCodeOption Option RootOutputFormatOption Option + RootProfileOption Option ) // 'pingcli request' command options var ( + RequestAccessTokenExpiryOption Option + RequestAccessTokenOption Option RequestDataOption Option RequestDataRawOption Option + RequestFailOption Option RequestHeaderOption Option RequestHTTPMethodOption Option RequestServiceOption Option - RequestAccessTokenOption Option - RequestAccessTokenExpiryOption Option - RequestFailOption Option ) diff --git a/internal/configuration/options/options_test.go b/internal/configuration/options/options_test.go index bd6e1c16..18bf7545 100644 --- a/internal/configuration/options/options_test.go +++ b/internal/configuration/options/options_test.go @@ -37,10 +37,10 @@ func Test_outputOptionsMDInfo(t *testing.T) { usageString = strings.ReplaceAll(usageString, "\n", "

") if !strings.Contains(option.KoanfKey, ".") { - propertyCategoryInformation["general"] = append(propertyCategoryInformation["general"], fmt.Sprintf("| %s | %s | %s | %s |", option.KoanfKey, option.Type, flagInfo, usageString)) + propertyCategoryInformation["general"] = append(propertyCategoryInformation["general"], fmt.Sprintf("| %s | %d | %s | %s |", option.KoanfKey, option.Type, flagInfo, usageString)) } else { rootKey := strings.Split(option.KoanfKey, ".")[0] - propertyCategoryInformation[rootKey] = append(propertyCategoryInformation[rootKey], fmt.Sprintf("| %s | %s | %s | %s |", option.KoanfKey, option.Type, flagInfo, usageString)) + propertyCategoryInformation[rootKey] = append(propertyCategoryInformation[rootKey], fmt.Sprintf("| %s | %d | %s | %s |", option.KoanfKey, option.Type, flagInfo, usageString)) } } diff --git a/internal/configuration/platform/export.go b/internal/configuration/platform/export.go index 8e27b108..1380f3c2 100644 --- a/internal/configuration/platform/export.go +++ b/internal/configuration/platform/export.go @@ -43,7 +43,7 @@ func initFormatOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_EXPORT_FORMAT, + Type: options.EXPORT_FORMAT, KoanfKey: "export.format", } } @@ -72,7 +72,7 @@ func initServiceGroupOption() { }, Sensitive: false, KoanfKey: "export.serviceGroup", - Type: options.ENUM_EXPORT_SERVICE_GROUP, + Type: options.EXPORT_SERVICE_GROUP, } } @@ -101,7 +101,7 @@ func initServicesOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_EXPORT_SERVICES, + Type: options.EXPORT_SERVICES, KoanfKey: "export.services", } } @@ -127,7 +127,7 @@ func initOutputDirectoryOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "export.outputDirectory", } } @@ -152,7 +152,7 @@ func initOverwriteOption() { }, Sensitive: false, KoanfKey: "export.overwrite", - Type: options.ENUM_BOOL, + Type: options.BOOL, } } @@ -173,6 +173,6 @@ func initPingOneEnvironmentIDOption() { Value: cobraValue, }, KoanfKey: "export.pingOne.environmentID", - Type: options.ENUM_UUID, + Type: options.UUID, } } diff --git a/internal/configuration/plugin/add.go b/internal/configuration/plugin/add.go index a30fe107..a85c5abd 100644 --- a/internal/configuration/plugin/add.go +++ b/internal/configuration/plugin/add.go @@ -21,7 +21,7 @@ func initPluginExecutablesOption() { EnvVar: "", // No env var Flag: nil, // No flag Sensitive: false, - Type: options.ENUM_STRING_SLICE, + Type: options.STRING_SLICE, KoanfKey: "plugins", } } diff --git a/internal/configuration/profiles/profiles.go b/internal/configuration/profiles/profiles.go index 24bedb11..dfbb2d32 100644 --- a/internal/configuration/profiles/profiles.go +++ b/internal/configuration/profiles/profiles.go @@ -19,7 +19,7 @@ func initDescriptionOption() { EnvVar: "", // No environment variable Flag: nil, // No flag Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "description", } } diff --git a/internal/configuration/request/request.go b/internal/configuration/request/request.go index bd3903fc..806974da 100644 --- a/internal/configuration/request/request.go +++ b/internal/configuration/request/request.go @@ -40,7 +40,7 @@ func initDataOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "", // No koanf key } } @@ -63,7 +63,7 @@ func initDataRawOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "", // No koanf key } } @@ -88,7 +88,7 @@ func initHeaderOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_HEADER, + Type: options.HEADER, KoanfKey: "", // No koanf key } } @@ -117,7 +117,7 @@ func initHTTPMethodOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_REQUEST_HTTP_METHOD, + Type: options.REQUEST_HTTP_METHOD, KoanfKey: "", // No koanf key } } @@ -146,7 +146,7 @@ func initServiceOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_REQUEST_SERVICE, + Type: options.REQUEST_SERVICE, KoanfKey: "request.service", } } @@ -161,7 +161,7 @@ func initAccessTokenOption() { EnvVar: "", // No environment variable Flag: nil, // No flag Sensitive: true, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "request.accessToken", } } @@ -176,7 +176,7 @@ func initAccessTokenExpiryOption() { EnvVar: "", // No environment variable Flag: nil, // No flag Sensitive: false, - Type: options.ENUM_INT, + Type: options.INT, KoanfKey: "request.accessTokenExpiry", } } @@ -198,7 +198,7 @@ func initFailOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_BOOL, + Type: options.BOOL, KoanfKey: "request.fail", } } diff --git a/internal/configuration/root/root.go b/internal/configuration/root/root.go index 057f6b5a..e51dea13 100644 --- a/internal/configuration/root/root.go +++ b/internal/configuration/root/root.go @@ -33,7 +33,7 @@ func initActiveProfileOption() { EnvVar: "", // No env var Flag: nil, // No flag Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "activeProfile", } } @@ -55,7 +55,7 @@ func initProfileOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "", // No koanf key } } @@ -77,7 +77,7 @@ func initColorOption() { NoOptDefVal: "true", // Make this flag a boolean flag }, Sensitive: false, - Type: options.ENUM_BOOL, + Type: options.BOOL, KoanfKey: "noColor", } } @@ -100,7 +100,7 @@ func initConfigOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "", // No koanf key } } @@ -126,7 +126,7 @@ func initDetailedExitCodeOption() { NoOptDefVal: "true", // Make this flag a boolean flag }, Sensitive: false, - Type: options.ENUM_BOOL, + Type: options.BOOL, KoanfKey: "detailedExitCode", } } @@ -154,7 +154,7 @@ func initOutputFormatOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_OUTPUT_FORMAT, + Type: options.OUTPUT_FORMAT, KoanfKey: "outputFormat", } } @@ -177,7 +177,7 @@ func initUnmaskSecretValuesOption() { NoOptDefVal: "true", // Make this flag a boolean flag }, Sensitive: false, - Type: options.ENUM_BOOL, + Type: options.BOOL, KoanfKey: "", // No KoanfKey } } diff --git a/internal/configuration/services/pingfederate.go b/internal/configuration/services/pingfederate.go index 59b96a1f..50e2fa76 100644 --- a/internal/configuration/services/pingfederate.go +++ b/internal/configuration/services/pingfederate.go @@ -45,7 +45,7 @@ func initHTTPSHostOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "service.pingFederate.httpsHost", } } @@ -68,7 +68,7 @@ func initAdminAPIPathOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "service.pingFederate.adminAPIPath", } } @@ -93,7 +93,7 @@ func initXBypassExternalValidationHeaderOption() { NoOptDefVal: "true", // Make this flag a boolean flag }, Sensitive: false, - Type: options.ENUM_BOOL, + Type: options.BOOL, KoanfKey: "service.pingFederate.xBypassExternalValidationHeader", } } @@ -118,7 +118,7 @@ func initCACertificatePemFilesOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING_SLICE, + Type: options.STRING_SLICE, KoanfKey: "service.pingFederate.caCertificatePEMFiles", } } @@ -143,7 +143,7 @@ func initInsecureTrustAllTLSOption() { NoOptDefVal: "true", // Make this flag a boolean flag }, Sensitive: false, - Type: options.ENUM_BOOL, + Type: options.BOOL, KoanfKey: "service.pingFederate.insecureTrustAllTLS", } } @@ -167,7 +167,7 @@ func initUsernameOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "service.pingFederate.authentication.basicAuth.username", } } @@ -190,7 +190,7 @@ func initPasswordOption() { Value: cobraValue, }, Sensitive: true, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "service.pingFederate.authentication.basicAuth.password", } } @@ -213,7 +213,7 @@ func initAccessTokenOption() { Value: cobraValue, }, Sensitive: true, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "service.pingFederate.authentication.accessTokenAuth.accessToken", } } @@ -236,7 +236,7 @@ func initClientIDOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "service.pingFederate.authentication.clientCredentialsAuth.clientID", } } @@ -259,7 +259,7 @@ func initClientSecretOption() { Value: cobraValue, }, Sensitive: true, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "service.pingFederate.authentication.clientCredentialsAuth.clientSecret", } } @@ -282,7 +282,7 @@ func initTokenURLOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "service.pingFederate.authentication.clientCredentialsAuth.tokenURL", } } @@ -308,7 +308,7 @@ func initScopesOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_STRING_SLICE, + Type: options.STRING_SLICE, KoanfKey: "service.pingFederate.authentication.clientCredentialsAuth.scopes", } } @@ -336,7 +336,7 @@ func initPingFederateAuthenticationTypeOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_PINGFEDERATE_AUTH_TYPE, + Type: options.PINGFEDERATE_AUTH_TYPE, KoanfKey: "service.pingFederate.authentication.type", } } diff --git a/internal/configuration/services/pingone.go b/internal/configuration/services/pingone.go index b0ee1713..7bd3e22c 100644 --- a/internal/configuration/services/pingone.go +++ b/internal/configuration/services/pingone.go @@ -36,7 +36,7 @@ func initAuthenticationWorkerClientIDOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_UUID, + Type: options.UUID, KoanfKey: "service.pingOne.authentication.worker.clientID", } } @@ -58,7 +58,7 @@ func initAuthenticationWorkerClientSecretOption() { Value: cobraValue, }, Sensitive: true, - Type: options.ENUM_STRING, + Type: options.STRING, KoanfKey: "service.pingOne.authentication.worker.clientSecret", } } @@ -81,7 +81,7 @@ func initAuthenticationWorkerEnvironmentIDOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_UUID, + Type: options.UUID, KoanfKey: "service.pingOne.authentication.worker.environmentID", } } @@ -108,7 +108,7 @@ func initPingOneAuthenticationTypeOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_PINGONE_AUTH_TYPE, + Type: options.PINGONE_AUTH_TYPE, KoanfKey: "service.pingOne.authentication.type", } } @@ -136,7 +136,7 @@ func initRegionCodeOption() { Value: cobraValue, }, Sensitive: false, - Type: options.ENUM_PINGONE_REGION_CODE, + Type: options.PINGONE_REGION_CODE, KoanfKey: "service.pingOne.regionCode", } } diff --git a/internal/customtypes/license_product.go b/internal/customtypes/license_product.go new file mode 100644 index 00000000..7de3e3bc --- /dev/null +++ b/internal/customtypes/license_product.go @@ -0,0 +1,80 @@ +// Copyright © 2025 Ping Identity Corporation + +package customtypes + +import ( + "fmt" + "slices" + "strings" + + "github.com/spf13/pflag" +) + +const ( + ENUM_LICENSE_PRODUCT_PING_ACCESS string = "pingaccess" + ENUM_LICENSE_PRODUCT_PING_AUTHORIZE string = "pingauthorize" + ENUM_LICENSE_PRODUCT_PING_AUTHORIZE_POLICY_EDITOR string = "pingauthorize-policy-editor" + ENUM_LICENSE_PRODUCT_PING_CENTRAL string = "pingcentral" + ENUM_LICENSE_PRODUCT_PING_DIRECTORY string = "pingdirectory" + ENUM_LICENSE_PRODUCT_PING_DIRECTORY_PROXY string = "pingdirectoryproxy" + ENUM_LICENSE_PRODUCT_PING_FEDERATE string = "pingfederate" +) + +type LicenseProduct string + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*LicenseProduct)(nil) + +// Implement pflag.Value interface for custom type in cobra MultiService parameter +func (lp *LicenseProduct) Set(product string) error { + if lp == nil { + return fmt.Errorf("failed to set LicenseProduct value: %s. LicenseProduct is nil", product) + } + + switch { + case strings.EqualFold(product, ENUM_LICENSE_PRODUCT_PING_ACCESS): + *lp = LicenseProduct(ENUM_LICENSE_PRODUCT_PING_ACCESS) + case strings.EqualFold(product, ENUM_LICENSE_PRODUCT_PING_AUTHORIZE): + *lp = LicenseProduct(ENUM_LICENSE_PRODUCT_PING_AUTHORIZE) + case strings.EqualFold(product, ENUM_LICENSE_PRODUCT_PING_AUTHORIZE_POLICY_EDITOR): + *lp = LicenseProduct(ENUM_LICENSE_PRODUCT_PING_AUTHORIZE_POLICY_EDITOR) + case strings.EqualFold(product, ENUM_LICENSE_PRODUCT_PING_CENTRAL): + *lp = LicenseProduct(ENUM_LICENSE_PRODUCT_PING_CENTRAL) + case strings.EqualFold(product, ENUM_LICENSE_PRODUCT_PING_DIRECTORY): + *lp = LicenseProduct(ENUM_LICENSE_PRODUCT_PING_DIRECTORY) + case strings.EqualFold(product, ENUM_LICENSE_PRODUCT_PING_DIRECTORY_PROXY): + *lp = LicenseProduct(ENUM_LICENSE_PRODUCT_PING_DIRECTORY_PROXY) + case strings.EqualFold(product, ENUM_LICENSE_PRODUCT_PING_FEDERATE): + *lp = LicenseProduct(ENUM_LICENSE_PRODUCT_PING_FEDERATE) + case strings.EqualFold(product, ""): // Allow empty string to be set + *lp = LicenseProduct("") + default: + return fmt.Errorf("unrecognized License Product: '%s'. Must be one of: %s", product, strings.Join(LicenseProductValidValues(), ", ")) + } + + return nil +} + +func (lp LicenseProduct) Type() string { + return "string" +} + +func (lp LicenseProduct) String() string { + return string(lp) +} + +func LicenseProductValidValues() []string { + products := []string{ + ENUM_LICENSE_PRODUCT_PING_ACCESS, + ENUM_LICENSE_PRODUCT_PING_AUTHORIZE, + ENUM_LICENSE_PRODUCT_PING_AUTHORIZE_POLICY_EDITOR, + ENUM_LICENSE_PRODUCT_PING_CENTRAL, + ENUM_LICENSE_PRODUCT_PING_DIRECTORY, + ENUM_LICENSE_PRODUCT_PING_DIRECTORY_PROXY, + ENUM_LICENSE_PRODUCT_PING_FEDERATE, + } + + slices.Sort(products) + + return products +} diff --git a/internal/customtypes/license_version.go b/internal/customtypes/license_version.go new file mode 100644 index 00000000..278f3aa7 --- /dev/null +++ b/internal/customtypes/license_version.go @@ -0,0 +1,46 @@ +// Copyright © 2025 Ping Identity Corporation + +package customtypes + +import ( + "fmt" + "regexp" + + "github.com/spf13/pflag" +) + +type LicenseVersion string + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*LicenseVersion)(nil) + +// Implement pflag.Value interface for custom type in cobra MultiService parameter +func (lp *LicenseVersion) Set(version string) error { + if lp == nil { + return fmt.Errorf("failed to set LicenseVersion value: %s. LicenseVersion is nil", version) + } + + // The license version must be of the form "major.minor" or empty + if version == "" { + *lp = LicenseVersion("") + + return nil + } + + // Validate the format of the version string via regex + if !regexp.MustCompile(`^\d+\.\d+$`).MatchString(version) { + return fmt.Errorf("failed to set LicenseVersion value: %s. Invalid version format, must be 'major.minor'. Example: '12.3'", version) + } + + *lp = LicenseVersion(version) + + return nil +} + +func (lp LicenseVersion) Type() string { + return "string" +} + +func (lp LicenseVersion) String() string { + return string(lp) +} diff --git a/internal/plugins/plugins.go b/internal/plugins/plugins.go index 89e5cdaa..0cb3ea03 100644 --- a/internal/plugins/plugins.go +++ b/internal/plugins/plugins.go @@ -33,6 +33,7 @@ func AddAllPluginToCmd(cmd *cobra.Command) error { } for pluginExecutable := range strings.SplitSeq(pluginExecutables, ",") { + pluginExecutable = strings.TrimSpace(pluginExecutable) if pluginExecutable == "" { continue } @@ -49,6 +50,7 @@ func AddAllPluginToCmd(cmd *cobra.Command) error { Example: conf.Example, DisableFlagsInUseLine: true, // We write our own flags in @Use attribute RunE: createCmdRunE(pluginExecutable), + DisableFlagParsing: true, // The plugin command will handle its own flags } cmd.AddCommand(pluginCmd) diff --git a/internal/profiles/validate.go b/internal/profiles/validate.go index 1d2c7664..c45f5d70 100644 --- a/internal/profiles/validate.go +++ b/internal/profiles/validate.go @@ -110,7 +110,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) vValue := profileKoanf.Get(key) switch opt.Type { - case options.ENUM_BOOL: + case options.BOOL: switch typedValue := vValue.(type) { case *customtypes.Bool: continue @@ -124,7 +124,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a boolean value", pName, typedValue, key) } - case options.ENUM_UUID: + case options.UUID: switch typedValue := vValue.(type) { case *customtypes.UUID: continue @@ -136,7 +136,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a UUID value", pName, typedValue, key) } - case options.ENUM_OUTPUT_FORMAT: + case options.OUTPUT_FORMAT: switch typedValue := vValue.(type) { case *customtypes.OutputFormat: continue @@ -148,7 +148,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an output format value", pName, typedValue, key) } - case options.ENUM_PINGONE_REGION_CODE: + case options.PINGONE_REGION_CODE: switch typedValue := vValue.(type) { case *customtypes.PingOneRegionCode: continue @@ -160,7 +160,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a PingOne Region Code value", pName, typedValue, key) } - case options.ENUM_STRING: + case options.STRING: switch typedValue := vValue.(type) { case *customtypes.String: continue @@ -172,7 +172,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a string value", pName, typedValue, key) } - case options.ENUM_STRING_SLICE: + case options.STRING_SLICE: switch typedValue := vValue.(type) { case *customtypes.StringSlice: continue @@ -196,7 +196,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a string slice value", pName, typedValue, key) } - case options.ENUM_EXPORT_SERVICE_GROUP: + case options.EXPORT_SERVICE_GROUP: switch typedValue := vValue.(type) { case *customtypes.ExportServiceGroup: continue @@ -208,7 +208,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a export service group value", pName, typedValue, key) } - case options.ENUM_EXPORT_SERVICES: + case options.EXPORT_SERVICES: switch typedValue := vValue.(type) { case *customtypes.ExportServices: continue @@ -232,7 +232,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a export service value", pName, typedValue, key) } - case options.ENUM_EXPORT_FORMAT: + case options.EXPORT_FORMAT: switch typedValue := vValue.(type) { case *customtypes.ExportFormat: continue @@ -244,7 +244,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an export format value", pName, typedValue, key) } - case options.ENUM_REQUEST_HTTP_METHOD: + case options.REQUEST_HTTP_METHOD: switch typedValue := vValue.(type) { case *customtypes.HTTPMethod: continue @@ -256,7 +256,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an HTTP method value", pName, typedValue, key) } - case options.ENUM_REQUEST_SERVICE: + case options.REQUEST_SERVICE: switch typedValue := vValue.(type) { case *customtypes.RequestService: continue @@ -268,7 +268,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a request service value", pName, typedValue, key) } - case options.ENUM_INT: + case options.INT: switch typedValue := vValue.(type) { case *customtypes.Int: continue @@ -284,7 +284,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an int value", pName, typedValue, key) } - case options.ENUM_PINGFEDERATE_AUTH_TYPE: + case options.PINGFEDERATE_AUTH_TYPE: switch typedValue := vValue.(type) { case *customtypes.PingFederateAuthenticationType: continue @@ -296,7 +296,7 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a PingFederate Authentication Type value", pName, typedValue, key) } - case options.ENUM_PINGONE_AUTH_TYPE: + case options.PINGONE_AUTH_TYPE: switch typedValue := vValue.(type) { case *customtypes.PingOneAuthenticationType: continue @@ -308,8 +308,32 @@ func validateProfileValues(pName string, profileKoanf *koanf.Koanf) (err error) default: return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a PingOne Authentication Type value", pName, typedValue, key) } + case options.LICENSE_PRODUCT: + switch typedValue := vValue.(type) { + case *customtypes.LicenseProduct: + continue + case string: + lp := new(customtypes.LicenseProduct) + if err = lp.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a License Product value: %w", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a License Product value", pName, typedValue, key) + } + case options.LICENSE_VERSION: + switch typedValue := vValue.(type) { + case *customtypes.LicenseVersion: + continue + case string: + lv := new(customtypes.LicenseVersion) + if err = lv.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a License Version value: %w", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a License Version value", pName, typedValue, key) + } default: - return fmt.Errorf("profile '%s': variable type '%s' for key '%s' is not recognized", pName, opt.Type, key) + return fmt.Errorf("profile '%s': variable type '%d' for key '%s' is not recognized", pName, opt.Type, key) } } diff --git a/internal/testing/testutils_koanf/koanf_utils.go b/internal/testing/testutils_koanf/koanf_utils.go index 57fca956..93318192 100644 --- a/internal/testing/testutils_koanf/koanf_utils.go +++ b/internal/testing/testutils_koanf/koanf_utils.go @@ -30,6 +30,9 @@ default: export: outputDirectory: %s services: ["%s"] + license: + devopsUser: %s + devopsKey: %s service: pingOne: regionCode: %s @@ -143,6 +146,8 @@ func getDefaultConfigFileContents() string { return fmt.Sprintf(defaultConfigFileContentsPattern, outputDirectoryReplacement, customtypes.ENUM_EXPORT_SERVICE_PINGONE_PROTECT, + os.Getenv("TEST_PINGCLI_DEVOPS_USER"), + os.Getenv("TEST_PINGCLI_DEVOPS_KEY"), os.Getenv("TEST_PINGONE_REGION_CODE"), os.Getenv("TEST_PINGONE_WORKER_CLIENT_ID"), os.Getenv("TEST_PINGONE_WORKER_CLIENT_SECRET"), From 29be88ed069fba4ad4c6e88b85f88f44d45980d1 Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Tue, 15 Jul 2025 09:16:04 -0600 Subject: [PATCH 2/3] Add Copyright headers --- internal/commands/license/license_internal.go | 2 ++ internal/commands/license/license_internal_test.go | 2 ++ internal/commands/plugin/add_internal.go | 2 ++ internal/commands/plugin/list_internal.go | 2 ++ internal/commands/plugin/remove_internal.go | 2 ++ internal/configuration/license/license.go | 2 ++ internal/customtypes/export_service_group.go | 2 ++ internal/customtypes/export_service_group_test.go | 2 ++ internal/profiles/koanf.go | 2 ++ shared/logger/logger.go | 2 ++ 10 files changed, 20 insertions(+) diff --git a/internal/commands/license/license_internal.go b/internal/commands/license/license_internal.go index 69582ada..dcf88c33 100644 --- a/internal/commands/license/license_internal.go +++ b/internal/commands/license/license_internal.go @@ -1,3 +1,5 @@ +// Copyright © 2025 Ping Identity Corporation + package license_internal import ( diff --git a/internal/commands/license/license_internal_test.go b/internal/commands/license/license_internal_test.go index 6fd138e1..fa3a2282 100644 --- a/internal/commands/license/license_internal_test.go +++ b/internal/commands/license/license_internal_test.go @@ -1,3 +1,5 @@ +// Copyright © 2025 Ping Identity Corporation + package license_internal import ( diff --git a/internal/commands/plugin/add_internal.go b/internal/commands/plugin/add_internal.go index 13cb34e6..cb599b59 100644 --- a/internal/commands/plugin/add_internal.go +++ b/internal/commands/plugin/add_internal.go @@ -1,3 +1,5 @@ +// Copyright © 2025 Ping Identity Corporation + package plugin_internal import ( diff --git a/internal/commands/plugin/list_internal.go b/internal/commands/plugin/list_internal.go index 9252807f..a4f90fb4 100644 --- a/internal/commands/plugin/list_internal.go +++ b/internal/commands/plugin/list_internal.go @@ -1,3 +1,5 @@ +// Copyright © 2025 Ping Identity Corporation + package plugin_internal import ( diff --git a/internal/commands/plugin/remove_internal.go b/internal/commands/plugin/remove_internal.go index 88971c6f..9a641761 100644 --- a/internal/commands/plugin/remove_internal.go +++ b/internal/commands/plugin/remove_internal.go @@ -1,3 +1,5 @@ +// Copyright © 2025 Ping Identity Corporation + package plugin_internal import ( diff --git a/internal/configuration/license/license.go b/internal/configuration/license/license.go index dacaea9b..9a7acb28 100644 --- a/internal/configuration/license/license.go +++ b/internal/configuration/license/license.go @@ -1,3 +1,5 @@ +// Copyright © 2025 Ping Identity Corporation + package configuration_license import ( diff --git a/internal/customtypes/export_service_group.go b/internal/customtypes/export_service_group.go index 3aa0184a..3f580a91 100644 --- a/internal/customtypes/export_service_group.go +++ b/internal/customtypes/export_service_group.go @@ -1,3 +1,5 @@ +// Copyright © 2025 Ping Identity Corporation + package customtypes import ( diff --git a/internal/customtypes/export_service_group_test.go b/internal/customtypes/export_service_group_test.go index 9d7783a3..ce887811 100644 --- a/internal/customtypes/export_service_group_test.go +++ b/internal/customtypes/export_service_group_test.go @@ -1,3 +1,5 @@ +// Copyright © 2025 Ping Identity Corporation + package customtypes_test import ( diff --git a/internal/profiles/koanf.go b/internal/profiles/koanf.go index 94215064..16ef763d 100644 --- a/internal/profiles/koanf.go +++ b/internal/profiles/koanf.go @@ -1,3 +1,5 @@ +// Copyright © 2025 Ping Identity Corporation + package profiles import ( diff --git a/shared/logger/logger.go b/shared/logger/logger.go index b575134e..9f01ab53 100644 --- a/shared/logger/logger.go +++ b/shared/logger/logger.go @@ -1,3 +1,5 @@ +// Copyright © 2025 Ping Identity Corporation + package logger import ( From 2cce5a0a4205fbf0d40b7d6a2169d19bca4166ce Mon Sep 17 00:00:00 2001 From: Erik Ostien Date: Tue, 15 Jul 2025 09:53:45 -0600 Subject: [PATCH 3/3] Fix workflow --- .github/workflows/code-analysis-lint-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-analysis-lint-test.yaml b/.github/workflows/code-analysis-lint-test.yaml index 3e22a569..7bb231bd 100644 --- a/.github/workflows/code-analysis-lint-test.yaml +++ b/.github/workflows/code-analysis-lint-test.yaml @@ -133,7 +133,7 @@ jobs: TEST_PINGONE_WORKER_CLIENT_SECRET: ${{ secrets.TEST_PINGONE_WORKER_CLIENT_SECRET }} TEST_PINGONE_REGION_CODE: ${{ secrets.TEST_PINGONE_REGION_CODE }} TEST_PINGCLI_DEVOPS_USER: ${{ secrets.TEST_PINGCLI_DEVOPS_USER }} - TEST_PINGCLI_DEVOPS_KEY: ${{ secrets.TEST_PINGCLI_DEVOPS_KEY + TEST_PINGCLI_DEVOPS_KEY: ${{ secrets.TEST_PINGCLI_DEVOPS_KEY }} steps: - uses: actions/checkout@v4