diff --git a/.github/workflows/code-analysis-lint-test.yaml b/.github/workflows/code-analysis-lint-test.yaml
index cee6e38f..7bb231bd 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..dcf88c33
--- /dev/null
+++ b/internal/commands/license/license_internal.go
@@ -0,0 +1,99 @@
+// Copyright © 2025 Ping Identity Corporation
+
+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..fa3a2282
--- /dev/null
+++ b/internal/commands/license/license_internal_test.go
@@ -0,0 +1,129 @@
+// Copyright © 2025 Ping Identity Corporation
+
+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/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/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..9a7acb28
--- /dev/null
+++ b/internal/configuration/license/license.go
@@ -0,0 +1,118 @@
+// Copyright © 2025 Ping Identity Corporation
+
+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/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/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/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/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"),
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 (