diff --git a/Makefile b/Makefile
index 3db5c84b..ac628f91 100644
--- a/Makefile
+++ b/Makefile
@@ -95,7 +95,7 @@ importfmtlint:
golangcilint:
@echo -n "Running 'golangci-lint' to check for code quality issues..."
@# Clear the cache for every run, so that the linter outputs the same results as the GH Actions workflow
- @if golangci-lint cache clear && golangci-lint run --timeout 5m ./...; then \
+ @if golangci-lint cache clean && golangci-lint run --timeout 5m ./...; then \
echo " SUCCESS"; \
else \
echo " FAILED"; \
diff --git a/cmd/config/list_keys_test.go b/cmd/config/list_keys_test.go
index 9020d211..15b0bb7e 100644
--- a/cmd/config/list_keys_test.go
+++ b/cmd/config/list_keys_test.go
@@ -54,6 +54,7 @@ func Example_listKeysValue() {
// - export.outputDirectory
// - export.overwrite
// - export.pingone.environmentID
+ // - export.serviceGroup
// - export.services
// - noColor
// - outputFormat
diff --git a/cmd/platform/export.go b/cmd/platform/export.go
index 605e46b9..a364bec5 100644
--- a/cmd/platform/export.go
+++ b/cmd/platform/export.go
@@ -27,6 +27,9 @@ const (
Export configuration-as-code packages for PingOne (core platform and SSO services).
pingcli platform export --services pingone-platform,pingone-sso
+ Export all configuration-as-code packages for PingOne. The --service-group flag can be used instead of listing all pingone-* packages in --services flag.
+ pingcli platform export --service-group pingone
+
Export configuration-as-code packages for PingOne (core platform), specifying the PingOne environment connection details.
pingcli platform export --services pingone-platform --pingone-client-environment-id 3cf2... --pingone-worker-client-id a719... --pingone-worker-client-secret ey..... --pingone-region-code EU
@@ -92,6 +95,7 @@ func exportRunE(cmd *cobra.Command, args []string) error {
func initGeneralExportFlags(cmd *cobra.Command) {
cmd.Flags().AddFlag(options.PlatformExportExportFormatOption.Flag)
cmd.Flags().AddFlag(options.PlatformExportServiceOption.Flag)
+ cmd.Flags().AddFlag(options.PlatformExportServiceGroupOption.Flag)
cmd.Flags().AddFlag(options.PlatformExportOutputDirectoryOption.Flag)
cmd.Flags().AddFlag(options.PlatformExportOverwriteOption.Flag)
cmd.Flags().AddFlag(options.PlatformExportPingOneEnvironmentIDOption.Flag)
diff --git a/cmd/platform/export_test.go b/cmd/platform/export_test.go
index cbb4f443..7d315c3b 100644
--- a/cmd/platform/export_test.go
+++ b/cmd/platform/export_test.go
@@ -48,8 +48,27 @@ func TestPlatformExportCmd_HelpFlag(t *testing.T) {
testutils.CheckExpectedError(t, err, nil)
}
+// Test Platform Export Command --service-group, -g flag
+func TestPlatformExportCmd_ServiceGroupFlag(t *testing.T) {
+ outputDir := t.TempDir()
+
+ err := testutils_cobra.ExecutePingcli(t, "platform", "export",
+ "--"+options.PlatformExportOutputDirectoryOption.CobraParamName, outputDir,
+ "--"+options.PlatformExportOverwriteOption.CobraParamName,
+ "--"+options.PlatformExportServiceGroupOption.CobraParamName, "pingone")
+ testutils.CheckExpectedError(t, err, nil)
+}
+
+// Test Platform Export Command --service-group with non-supported service group
+func TestPlatformExportCmd_ServiceGroupFlagInvalidServiceGroup(t *testing.T) {
+ expectedErrorPattern := `^invalid argument ".*" for "-g, --service-group" flag: unrecognized service group '.*'\. Must be one of: .*$`
+ err := testutils_cobra.ExecutePingcli(t, "platform", "export",
+ "--"+options.PlatformExportServiceGroupOption.CobraParamName, "invalid")
+ testutils.CheckExpectedError(t, err, &expectedErrorPattern)
+}
+
// Test Platform Export Command --services flag
-func TestPlatformExportCmd_ServiceFlag(t *testing.T) {
+func TestPlatformExportCmd_ServicesFlag(t *testing.T) {
outputDir := t.TempDir()
err := testutils_cobra.ExecutePingcli(t, "platform", "export",
@@ -60,7 +79,7 @@ func TestPlatformExportCmd_ServiceFlag(t *testing.T) {
}
// Test Platform Export Command --services flag with invalid service
-func TestPlatformExportCmd_ServiceFlagInvalidService(t *testing.T) {
+func TestPlatformExportCmd_ServicesFlagInvalidService(t *testing.T) {
expectedErrorPattern := `^invalid argument ".*" for "-s, --services" flag: failed to set ExportServices: Invalid service: .*\. Allowed services: .*$`
err := testutils_cobra.ExecutePingcli(t, "platform", "export",
"--"+options.PlatformExportServiceOption.CobraParamName, "invalid")
diff --git a/docs/tool-configuration/configuration-key.md b/docs/tool-configuration/configuration-key.md
index 50011f67..a9bfd442 100644
--- a/docs/tool-configuration/configuration-key.md
+++ b/docs/tool-configuration/configuration-key.md
@@ -42,6 +42,7 @@ The following parameters can be configured in Ping CLI's static configuration fi
| export.outputDirectory | ENUM_STRING | --output-directory / -d | Specifies the output directory for export. Example: `$HOME/pingcli-export` |
| export.overwrite | ENUM_BOOL | --overwrite / -o | Overwrite the existing generated exports in output directory. |
| export.pingone.environmentID | ENUM_UUID | --pingone-export-environment-id | The ID of the PingOne environment to export. Must be a valid PingOne UUID. |
+| export.serviceGroup | ENUM_EXPORT_SERVICE_GROUP | --service-group / -g | Specifies the service group to export.
Options are: pingone.
Example: `pingone` |
| export.services | ENUM_EXPORT_SERVICES | --services / -s | Specifies the service(s) to export. Accepts a comma-separated string to delimit multiple services.
Options are: pingfederate, pingone-mfa, pingone-platform, pingone-protect, pingone-sso.
Example: `pingone-sso,pingone-mfa,pingfederate` |
#### Custom Request Properties
diff --git a/internal/commands/config/set_internal.go b/internal/commands/config/set_internal.go
index 5418d35e..e8c0af25 100644
--- a/internal/commands/config/set_internal.go
+++ b/internal/commands/config/set_internal.go
@@ -123,6 +123,12 @@ func setValue(profileViper *viper.Viper, vKey, vValue string, valueType options.
return fmt.Errorf("value for key '%s' must be a valid export format. Allowed [%s]: %v", vKey, strings.Join(customtypes.ExportFormatValidValues(), ", "), err)
}
profileViper.Set(vKey, exportFormat)
+ case options.ENUM_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]: %v", vKey, strings.Join(customtypes.ExportServiceGroupValidValues(), ", "), err)
+ }
+ profileViper.Set(vKey, exportServiceGroup)
case options.ENUM_EXPORT_SERVICES:
exportServices := new(customtypes.ExportServices)
if err = exportServices.Set(vValue); err != nil {
diff --git a/internal/commands/platform/export_internal.go b/internal/commands/platform/export_internal.go
index 927f8434..647d8f14 100644
--- a/internal/commands/platform/export_internal.go
+++ b/internal/commands/platform/export_internal.go
@@ -50,6 +50,10 @@ func RunInternalExport(ctx context.Context, commandVersion string) (err error) {
if err != nil {
return err
}
+ exportServiceGroup, err := profiles.GetOptionValue(options.PlatformExportServiceGroupOption)
+ if err != nil {
+ return err
+ }
exportServices, err := profiles.GetOptionValue(options.PlatformExportServiceOption)
if err != nil {
return err
@@ -63,11 +67,26 @@ func RunInternalExport(ctx context.Context, commandVersion string) (err error) {
return err
}
+ var exportableConnectors *[]connector.Exportable
es := new(customtypes.ExportServices)
if err = es.Set(exportServices); err != nil {
return err
}
+ esg := new(customtypes.ExportServiceGroup)
+ if err = esg.Set(exportServiceGroup); err != nil {
+ return err
+ }
+
+ es2 := new(customtypes.ExportServices)
+ if err = es2.SetServicesByServiceGroup(esg); err != nil {
+ return err
+ }
+
+ if err = es.Merge(*es2); err != nil {
+ return err
+ }
+
if es.ContainsPingOneService() {
if err = initPingOneServices(ctx, commandVersion); err != nil {
return err
@@ -80,6 +99,8 @@ func RunInternalExport(ctx context.Context, commandVersion string) (err error) {
}
}
+ exportableConnectors = getExportableConnectors(es)
+
overwriteExportBool, err := strconv.ParseBool(overwriteExport)
if err != nil {
return err
@@ -88,8 +109,6 @@ func RunInternalExport(ctx context.Context, commandVersion string) (err error) {
return err
}
- exportableConnectors := getExportableConnectors(es)
-
if err := exportConnectors(exportableConnectors, exportFormat, outputDir, overwriteExportBool); err != nil {
return err
}
diff --git a/internal/configuration/options/options.go b/internal/configuration/options/options.go
index ab8ce905..73181203 100644
--- a/internal/configuration/options/options.go
+++ b/internal/configuration/options/options.go
@@ -16,6 +16,7 @@ const (
ENUM_BOOL OptionType = "ENUM_BOOL"
ENUM_EXPORT_FORMAT OptionType = "ENUM_EXPORT_FORMAT"
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"
@@ -48,6 +49,7 @@ func Options() []Option {
PingOneRegionCodeOption,
PlatformExportExportFormatOption,
+ PlatformExportServiceGroupOption,
PlatformExportServiceOption,
PlatformExportOutputDirectoryOption,
PlatformExportOverwriteOption,
@@ -142,6 +144,7 @@ var (
var (
PlatformExportExportFormatOption Option
PlatformExportServiceOption Option
+ PlatformExportServiceGroupOption Option
PlatformExportOutputDirectoryOption Option
PlatformExportOverwriteOption Option
PlatformExportPingOneEnvironmentIDOption Option
diff --git a/internal/configuration/platform/export.go b/internal/configuration/platform/export.go
index 325a90da..40609728 100644
--- a/internal/configuration/platform/export.go
+++ b/internal/configuration/platform/export.go
@@ -14,6 +14,7 @@ import (
func InitPlatformExportOptions() {
initFormatOption()
initServicesOption()
+ initServiceGroupOption()
initOutputDirectoryOption()
initOverwriteOption()
initPingOneEnvironmentIDOption()
@@ -47,12 +48,39 @@ func initFormatOption() {
}
}
+func initServiceGroupOption() {
+ cobraParamName := "service-group"
+ cobraValue := new(customtypes.ExportServiceGroup)
+ defaultValue := customtypes.ExportServiceGroup("")
+ envVar := "PINGCLI_EXPORT_SERVICE_GROUP"
+ options.PlatformExportServiceGroupOption = options.Option{
+ CobraParamName: cobraParamName,
+ CobraParamValue: cobraValue,
+ DefaultValue: &defaultValue,
+ EnvVar: envVar,
+ Flag: &pflag.Flag{
+ Name: cobraParamName,
+ Shorthand: "g",
+ Usage: fmt.Sprintf(
+ "Specifies the service group to export. "+
+ "\nOptions are: %s."+
+ "\nExample: '%s'",
+ strings.Join(customtypes.ExportServiceGroupValidValues(), ", "),
+ string(customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE),
+ ),
+ Value: cobraValue,
+ },
+ Sensitive: false,
+ Type: options.ENUM_EXPORT_SERVICE_GROUP,
+ ViperKey: "export.serviceGroup",
+ }
+}
+
func initServicesOption() {
cobraParamName := "services"
cobraValue := new(customtypes.ExportServices)
- defaultValue := customtypes.ExportServices(customtypes.ExportServicesValidValues())
+ defaultValue := customtypes.ExportServices([]string{})
envVar := "PINGCLI_EXPORT_SERVICES"
-
options.PlatformExportServiceOption = options.Option{
CobraParamName: cobraParamName,
CobraParamValue: cobraValue,
@@ -63,11 +91,9 @@ func initServicesOption() {
Shorthand: "s",
Usage: fmt.Sprintf(
"Specifies the service(s) to export. Accepts a comma-separated string to delimit multiple services. "+
- "(default %s)"+
"\nOptions are: %s."+
"\nExample: '%s,%s,%s'",
strings.Join(customtypes.ExportServicesValidValues(), ", "),
- strings.Join(customtypes.ExportServicesValidValues(), ", "),
string(customtypes.ENUM_EXPORT_SERVICE_PINGONE_SSO),
string(customtypes.ENUM_EXPORT_SERVICE_PINGONE_MFA),
string(customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE),
diff --git a/internal/customtypes/export_service_group.go b/internal/customtypes/export_service_group.go
new file mode 100644
index 00000000..3cb79b00
--- /dev/null
+++ b/internal/customtypes/export_service_group.go
@@ -0,0 +1,54 @@
+package customtypes
+
+import (
+ "fmt"
+ "slices"
+ "strings"
+
+ "github.com/spf13/pflag"
+)
+
+const (
+ ENUM_EXPORT_SERVICE_GROUP_PINGONE string = "pingone"
+)
+
+type ExportServiceGroup string
+
+// Verify that the custom type satisfies the pflag.Value interface
+var _ pflag.Value = (*ExportServiceGroup)(nil)
+
+func (esg *ExportServiceGroup) Set(serviceGroup string) error {
+ if esg == nil {
+ return fmt.Errorf("failed to set ExportServiceGroup value: %s. ExportServiceGroup is nil", serviceGroup)
+ }
+
+ if serviceGroup == "" {
+ return nil
+ }
+
+ switch {
+ case strings.EqualFold(ENUM_EXPORT_SERVICE_GROUP_PINGONE, serviceGroup):
+ *esg = ExportServiceGroup(ENUM_EXPORT_SERVICE_GROUP_PINGONE)
+ default:
+ return fmt.Errorf("unrecognized service group '%s'. Must be one of: %s", serviceGroup, strings.Join(ExportServiceGroupValidValues(), ", "))
+ }
+ return nil
+}
+
+func (esg ExportServiceGroup) Type() string {
+ return "string"
+}
+
+func (esg ExportServiceGroup) String() string {
+ return string(esg)
+}
+
+func ExportServiceGroupValidValues() []string {
+ validServiceGroups := []string{
+ ENUM_EXPORT_SERVICE_GROUP_PINGONE,
+ }
+
+ slices.Sort(validServiceGroups)
+
+ return validServiceGroups
+}
diff --git a/internal/customtypes/export_service_group_test.go b/internal/customtypes/export_service_group_test.go
new file mode 100644
index 00000000..9d7783a3
--- /dev/null
+++ b/internal/customtypes/export_service_group_test.go
@@ -0,0 +1,52 @@
+package customtypes_test
+
+import (
+ "testing"
+
+ "github.com/pingidentity/pingcli/internal/customtypes"
+ "github.com/pingidentity/pingcli/internal/testing/testutils"
+)
+
+// Test ExportServiceGroup Set function
+func Test_ExportServiceGroup_Set(t *testing.T) {
+ // Create a new ExportServiceGroup
+ esg := new(customtypes.ExportServiceGroup)
+
+ err := esg.Set(customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE)
+ if err != nil {
+ t.Errorf("Set returned error: %v", err)
+ }
+}
+
+// Test ExportServiceGroup Set function fails with invalid value
+func Test_ExportServiceGroup_Set_InvalidValue(t *testing.T) {
+ // Create a new ExportServiceGroup
+ esg := new(customtypes.ExportServiceGroup)
+
+ invalidValue := "invalid"
+
+ expectedErrorPattern := `^unrecognized service group .*\. Must be one of: .*$`
+ err := esg.Set(invalidValue)
+ testutils.CheckExpectedError(t, err, &expectedErrorPattern)
+}
+
+// Test ExportServiceGroup Set function fails with nil
+func Test_ExportServiceGroup_Set_Nil(t *testing.T) {
+ var esg *customtypes.ExportServiceGroup
+
+ val := customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE
+
+ expectedErrorPattern := `^failed to set ExportServiceGroup value: .* ExportServiceGroup is nil$`
+ err := esg.Set(val)
+ testutils.CheckExpectedError(t, err, &expectedErrorPattern)
+}
+
+// Test ExportServiceGroup Valid Values returns expected amount
+func Test_ExportServiceGroupValidValues(t *testing.T) {
+ serviceGroupEnum := customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE
+
+ serviceGroupValidValues := customtypes.ExportServiceGroupValidValues()
+ if serviceGroupValidValues[0] != serviceGroupEnum {
+ t.Errorf("ExportServiceGroupValidValues returned: %v, expected: %v", serviceGroupValidValues, serviceGroupEnum)
+ }
+}
diff --git a/internal/customtypes/export_services.go b/internal/customtypes/export_services.go
index 098a8044..c3cfe2f8 100644
--- a/internal/customtypes/export_services.go
+++ b/internal/customtypes/export_services.go
@@ -41,11 +41,14 @@ func (es *ExportServices) Set(services string) error {
validServices := ExportServicesValidValues()
serviceList := strings.Split(services, ",")
+ returnServiceList := []string{}
- for i, service := range serviceList {
+ for _, service := range serviceList {
if !slices.ContainsFunc(validServices, func(validService string) bool {
if strings.EqualFold(validService, service) {
- serviceList[i] = validService
+ if !slices.Contains(returnServiceList, validService) {
+ returnServiceList = append(returnServiceList, validService)
+ }
return true
}
return false
@@ -54,24 +57,31 @@ func (es *ExportServices) Set(services string) error {
}
}
- slices.Sort(serviceList)
+ slices.Sort(returnServiceList)
- *es = ExportServices(serviceList)
+ *es = ExportServices(returnServiceList)
return nil
}
+func (es *ExportServices) SetServicesByServiceGroup(serviceGroup *ExportServiceGroup) error {
+ if es == nil {
+ return fmt.Errorf("failed to set ExportServices value: %s. ExportServices is nil", serviceGroup)
+ }
+
+ switch {
+ case strings.EqualFold(ENUM_EXPORT_SERVICE_GROUP_PINGONE, serviceGroup.String()):
+ return es.Set(strings.Join(ExportServicesPingOneValidValues(), ","))
+ default:
+ return fmt.Errorf("failed to SetServicesByServiceGroup: Invalid service group: %s. Allowed services: %s", serviceGroup.String(), strings.Join(ExportServiceGroupValidValues(), ", "))
+ }
+}
+
func (es ExportServices) ContainsPingOneService() bool {
if es == nil {
return false
}
- pingoneServices := []string{
- ENUM_EXPORT_SERVICE_PINGONE_PLATFORM,
- ENUM_EXPORT_SERVICE_PINGONE_AUTHORIZE,
- ENUM_EXPORT_SERVICE_PINGONE_SSO,
- ENUM_EXPORT_SERVICE_PINGONE_MFA,
- ENUM_EXPORT_SERVICE_PINGONE_PROTECT,
- }
+ pingoneServices := ExportServicesPingOneValidValues()
for _, service := range es {
if slices.ContainsFunc(pingoneServices, func(s string) bool {
@@ -114,3 +124,30 @@ func ExportServicesValidValues() []string {
return allServices
}
+
+func ExportServicesPingOneValidValues() []string {
+ pingOneServices := []string{
+ ENUM_EXPORT_SERVICE_PINGONE_PLATFORM,
+ ENUM_EXPORT_SERVICE_PINGONE_AUTHORIZE,
+ ENUM_EXPORT_SERVICE_PINGONE_SSO,
+ ENUM_EXPORT_SERVICE_PINGONE_MFA,
+ ENUM_EXPORT_SERVICE_PINGONE_PROTECT,
+ }
+
+ slices.Sort(pingOneServices)
+
+ return pingOneServices
+}
+
+func (es *ExportServices) Merge(es2 ExportServices) error {
+ mergedServices := []string{}
+
+ for _, service := range append(es.GetServices(), es2.GetServices()...) {
+ if !slices.Contains(mergedServices, service) {
+ mergedServices = append(mergedServices, service)
+ }
+ }
+
+ slices.Sort(mergedServices)
+ return es.Set(strings.Join(mergedServices, ","))
+}
diff --git a/internal/customtypes/export_services_test.go b/internal/customtypes/export_services_test.go
index 1e5c0051..f78df857 100644
--- a/internal/customtypes/export_services_test.go
+++ b/internal/customtypes/export_services_test.go
@@ -95,3 +95,11 @@ func Test_ExportServices_String(t *testing.T) {
t.Errorf("String returned: %s, expected: %s", actual, expected)
}
}
+
+// Test ExportServicePingOneValidValues
+func Test_ExportServicesPingOneValidValues(t *testing.T) {
+ pingOneServiceGroupValidValues := customtypes.ExportServicesPingOneValidValues()
+ if len(pingOneServiceGroupValidValues) != 5 {
+ t.Errorf("ExportServicesPingOneValidValues returned: %v, expected: %v", len(pingOneServiceGroupValidValues), 5)
+ }
+}
diff --git a/internal/profiles/validate.go b/internal/profiles/validate.go
index 865f6b5c..8f2ec38d 100644
--- a/internal/profiles/validate.go
+++ b/internal/profiles/validate.go
@@ -185,6 +185,18 @@ func validateProfileValues(pName string, profileViper *viper.Viper) (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:
+ switch typedValue := vValue.(type) {
+ case *customtypes.ExportServiceGroup:
+ continue
+ case string:
+ esg := new(customtypes.ExportServiceGroup)
+ if err = esg.Set(typedValue); err != nil {
+ return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a export service group value: %v", pName, typedValue, key, err)
+ }
+ 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:
switch typedValue := vValue.(type) {
case *customtypes.ExportServices:
diff --git a/internal/testing/testutils_viper/viper_utils.go b/internal/testing/testutils_viper/viper_utils.go
index e61db85e..3ab6e60a 100644
--- a/internal/testing/testutils_viper/viper_utils.go
+++ b/internal/testing/testutils_viper/viper_utils.go
@@ -10,6 +10,7 @@ import (
"github.com/pingidentity/pingcli/internal/configuration"
"github.com/pingidentity/pingcli/internal/configuration/options"
+ "github.com/pingidentity/pingcli/internal/customtypes"
"github.com/pingidentity/pingcli/internal/profiles"
)
@@ -26,6 +27,8 @@ default:
outputFormat: text
export:
outputDirectory: %s
+ serviceGroup: %s
+ services: ["%s"]
service:
pingone:
regionCode: %s
@@ -110,6 +113,8 @@ func InitVipersCustomFile(t *testing.T, fileContents string) {
func getDefaultConfigFileContents() string {
return fmt.Sprintf(defaultConfigFileContentsPattern,
outputDirectoryReplacement,
+ customtypes.ENUM_EXPORT_SERVICE_GROUP_PINGONE,
+ customtypes.ENUM_EXPORT_SERVICE_PINGFEDERATE,
os.Getenv(options.PingOneRegionCodeOption.EnvVar),
os.Getenv(options.PingOneAuthenticationWorkerClientIDOption.EnvVar),
os.Getenv(options.PingOneAuthenticationWorkerClientSecretOption.EnvVar),