From 845612c6ba206a1a5325fc5e95425d27bd668eeb Mon Sep 17 00:00:00 2001 From: Jeremy Gooch Date: Fri, 19 Jun 2026 13:56:38 -0500 Subject: [PATCH 1/2] CSTM-190: Adding the new ui_plugins command group package and stubbing out future subcommands --- cmd/api/test_data/test_create.json | 2 +- cmd/api/test_data/test_update.json | 2 +- cmd/root/root.go | 2 + cmd/root/root_test.go | 2 +- cmd/ui_plugins/create.go | 18 +++++++++ cmd/ui_plugins/delete.go | 18 +++++++++ cmd/ui_plugins/init.go | 18 +++++++++ cmd/ui_plugins/link.go | 18 +++++++++ cmd/ui_plugins/list.go | 18 +++++++++ cmd/ui_plugins/ui_plugins.go | 59 +++++++++++++++++++++++++++ cmd/ui_plugins/ui_plugins.md | 27 +++++++++++++ cmd/ui_plugins/ui_plugins_test.go | 64 ++++++++++++++++++++++++++++++ cmd/ui_plugins/update.go | 18 +++++++++ cmd/ui_plugins/upload.go | 18 +++++++++ 14 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 cmd/ui_plugins/create.go create mode 100644 cmd/ui_plugins/delete.go create mode 100644 cmd/ui_plugins/init.go create mode 100644 cmd/ui_plugins/link.go create mode 100644 cmd/ui_plugins/list.go create mode 100644 cmd/ui_plugins/ui_plugins.go create mode 100644 cmd/ui_plugins/ui_plugins.md create mode 100644 cmd/ui_plugins/ui_plugins_test.go create mode 100644 cmd/ui_plugins/update.go create mode 100644 cmd/ui_plugins/upload.go diff --git a/cmd/api/test_data/test_create.json b/cmd/api/test_data/test_create.json index e43c95fc..bb058d04 100644 --- a/cmd/api/test_data/test_create.json +++ b/cmd/api/test_data/test_create.json @@ -1 +1 @@ -{"attributes":{"accountFilter":"!(nativeIdentity.startsWith(\"*DELETED*\"))","accountPropertyFilter":"(groups.containsAll({'Admin'}) || location == 'Austin')","accountReturnFirstLink":false,"accountSortAttribute":"created","accountSortDescending":false,"attributeName":"DEPARTMENT","input":{"attributes":{"attributeName":"first_name","sourceName":"Source"},"type":"accountAttribute"},"requiresPeriodicRefresh":false,"sourceName":"Workday"},"name":"PRithHMFRWGzvlna","type":"dateFormat"} \ No newline at end of file +{"attributes":{"accountFilter":"!(nativeIdentity.startsWith(\"*DELETED*\"))","accountPropertyFilter":"(groups.containsAll({'Admin'}) || location == 'Austin')","accountReturnFirstLink":false,"accountSortAttribute":"created","accountSortDescending":false,"attributeName":"DEPARTMENT","input":{"attributes":{"attributeName":"first_name","sourceName":"Source"},"type":"accountAttribute"},"requiresPeriodicRefresh":false,"sourceName":"Workday"},"name":"oNYjBXFWwyqnZfqF","type":"dateFormat"} \ No newline at end of file diff --git a/cmd/api/test_data/test_update.json b/cmd/api/test_data/test_update.json index 47da2818..90a5e878 100644 --- a/cmd/api/test_data/test_update.json +++ b/cmd/api/test_data/test_update.json @@ -1 +1 @@ -{"attributes":{"accountFilter":"!(nativeIdentity.startsWith(\"*DELETED*\"))","accountPropertyFilter":"(groups.containsAll({'Admin'}) || location == 'Austin')","accountReturnFirstLink":false,"accountSortAttribute":"created","accountSortDescending":false,"attributeName":"UPDATED_DEPARTMENT","input":{"attributes":{"attributeName":"first_name","sourceName":"Source"},"type":"accountAttribute"},"requiresPeriodicRefresh":false,"sourceName":"Workday"},"name":"xQTfLofnWtrDsrbX","type":"dateFormat"} \ No newline at end of file +{"attributes":{"accountFilter":"!(nativeIdentity.startsWith(\"*DELETED*\"))","accountPropertyFilter":"(groups.containsAll({'Admin'}) || location == 'Austin')","accountReturnFirstLink":false,"accountSortAttribute":"created","accountSortDescending":false,"attributeName":"UPDATED_DEPARTMENT","input":{"attributes":{"attributeName":"first_name","sourceName":"Source"},"type":"accountAttribute"},"requiresPeriodicRefresh":false,"sourceName":"Workday"},"name":"oNYjBXFWwyqnZfqF","type":"dateFormat"} \ No newline at end of file diff --git a/cmd/root/root.go b/cmd/root/root.go index e6a6b3de..9189ba5d 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -17,6 +17,7 @@ import ( "github.com/sailpoint-oss/sailpoint-cli/cmd/set" "github.com/sailpoint-oss/sailpoint-cli/cmd/spconfig" "github.com/sailpoint-oss/sailpoint-cli/cmd/transform" + "github.com/sailpoint-oss/sailpoint-cli/cmd/ui_plugins" "github.com/sailpoint-oss/sailpoint-cli/cmd/va" "github.com/sailpoint-oss/sailpoint-cli/cmd/workflow" "github.com/sailpoint-oss/sailpoint-cli/internal/terminal" @@ -69,6 +70,7 @@ func NewRootCommand() *cobra.Command { workflow.NewWorkflowCommand(), sanitize.NewSanitizeCommand(), reassign.NewReassignCommand(), + ui_plugins.NewUIPluginsCommand(), ) root.PersistentFlags().StringVarP(&env, "env", "", "", "Environment to use for SailPoint CLI commands") diff --git a/cmd/root/root_test.go b/cmd/root/root_test.go index a1f34e86..121ca492 100644 --- a/cmd/root/root_test.go +++ b/cmd/root/root_test.go @@ -13,7 +13,7 @@ import ( // Expected number of subcommands to `sail` root command const ( - numRootSubcommands = 16 + numRootSubcommands = 17 ) func TestNewRootCmd_noArgs(t *testing.T) { diff --git a/cmd/ui_plugins/create.go b/cmd/ui_plugins/create.go new file mode 100644 index 00000000..2064cfaf --- /dev/null +++ b/cmd/ui_plugins/create.go @@ -0,0 +1,18 @@ +// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved. +package ui_plugins + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newCreateCommand() *cobra.Command { + return &cobra.Command{ + Use: "create", + Short: "Create a UI plugin instance in the current tenant", + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("`sail ui-plugins create` is not implemented yet") + }, + } +} diff --git a/cmd/ui_plugins/delete.go b/cmd/ui_plugins/delete.go new file mode 100644 index 00000000..14fb3997 --- /dev/null +++ b/cmd/ui_plugins/delete.go @@ -0,0 +1,18 @@ +// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved. +package ui_plugins + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newDeleteCommand() *cobra.Command { + return &cobra.Command{ + Use: "delete", + Short: "Delete a UI plugin instance", + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("`sail ui-plugins delete` is not implemented yet") + }, + } +} diff --git a/cmd/ui_plugins/init.go b/cmd/ui_plugins/init.go new file mode 100644 index 00000000..b9d078c9 --- /dev/null +++ b/cmd/ui_plugins/init.go @@ -0,0 +1,18 @@ +// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved. +package ui_plugins + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newInitCommand() *cobra.Command { + return &cobra.Command{ + Use: "init", + Short: "Initialize a UI plugin workspace", + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("`sail ui-plugins init` is not implemented yet") + }, + } +} diff --git a/cmd/ui_plugins/link.go b/cmd/ui_plugins/link.go new file mode 100644 index 00000000..691a68f5 --- /dev/null +++ b/cmd/ui_plugins/link.go @@ -0,0 +1,18 @@ +// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved. +package ui_plugins + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newLinkCommand() *cobra.Command { + return &cobra.Command{ + Use: "link", + Short: "Link local development URL for a UI plugin", + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("`sail ui-plugins link` is not implemented yet") + }, + } +} diff --git a/cmd/ui_plugins/list.go b/cmd/ui_plugins/list.go new file mode 100644 index 00000000..9bae55c1 --- /dev/null +++ b/cmd/ui_plugins/list.go @@ -0,0 +1,18 @@ +// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved. +package ui_plugins + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newListCommand() *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List UI plugin instances in the current tenant", + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("`sail ui-plugins list` is not implemented yet") + }, + } +} diff --git a/cmd/ui_plugins/ui_plugins.go b/cmd/ui_plugins/ui_plugins.go new file mode 100644 index 00000000..00f71133 --- /dev/null +++ b/cmd/ui_plugins/ui_plugins.go @@ -0,0 +1,59 @@ +// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved. +package ui_plugins + +import ( + _ "embed" + "fmt" + "os" + + "github.com/sailpoint-oss/sailpoint-cli/internal/util" + "github.com/spf13/cobra" +) + +const experimentalUIPluginsEnvVar = "SAIL_EXPERIMENTAL_UI_PLUGINS" + +//go:embed ui_plugins.md +var uiPluginsHelp string + +func NewUIPluginsCommand() *cobra.Command { + help := util.ParseHelp(uiPluginsHelp) + cmd := &cobra.Command{ + Use: "ui-plugins", + Short: "Manage UI plugin workflows in Identity Security Cloud", + Long: help.Long, + Example: help.Example, + Hidden: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if !isUIPluginsEnabled() { + return experimentalDisabledError() + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + + cmd.AddCommand( + newInitCommand(), + newCreateCommand(), + newLinkCommand(), + newUpdateCommand(), + newUploadCommand(), + newListCommand(), + newDeleteCommand(), + ) + + return cmd +} + +func isUIPluginsEnabled() bool { + return os.Getenv(experimentalUIPluginsEnvVar) == "1" +} + +func experimentalDisabledError() error { + return fmt.Errorf( + "the `sail ui-plugins` command group is experimental and currently disabled. Enable it with `%s=1`", + experimentalUIPluginsEnvVar, + ) +} diff --git a/cmd/ui_plugins/ui_plugins.md b/cmd/ui_plugins/ui_plugins.md new file mode 100644 index 00000000..062f0f2a --- /dev/null +++ b/cmd/ui_plugins/ui_plugins.md @@ -0,0 +1,27 @@ +==Long== +# UI Plugins +Manage UI Plugin workflows in Identity Security Cloud. + +This command group is currently experimental and hidden from default command discovery. +Enable it for development with: + +```bash +export SAIL_EXPERIMENTAL_UI_PLUGINS=1 +``` + +Planned workflow commands: +- init +- create +- link +- update +- upload +- list +- delete +==== + +==Example== +```bash +sail ui-plugins +SAIL_EXPERIMENTAL_UI_PLUGINS=1 sail ui-plugins init +``` +==== diff --git a/cmd/ui_plugins/ui_plugins_test.go b/cmd/ui_plugins/ui_plugins_test.go new file mode 100644 index 00000000..4c88e4e6 --- /dev/null +++ b/cmd/ui_plugins/ui_plugins_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved. +package ui_plugins + +import ( + "strings" + "testing" +) + +func TestNewUIPluginsCommandStructure(t *testing.T) { + cmd := NewUIPluginsCommand() + + if cmd.Use != "ui-plugins" { + t.Fatalf("expected use to be ui-plugins, got %s", cmd.Use) + } + + if !cmd.Hidden { + t.Fatal("expected ui-plugins command to be hidden") + } + + if len(cmd.Commands()) != 7 { + t.Fatalf("expected 7 subcommands, got %d", len(cmd.Commands())) + } +} + +func TestUIPluginsGateDisabled(t *testing.T) { + t.Setenv(experimentalUIPluginsEnvVar, "") + cmd := NewUIPluginsCommand() + cmd.SetArgs([]string{}) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected command to fail when experimental gate is disabled") + } + + errText := err.Error() + if !strings.Contains(errText, "experimental") || !strings.Contains(errText, experimentalUIPluginsEnvVar) { + t.Fatalf("expected error to mention experimental gate and env var, got: %s", errText) + } +} + +func TestUIPluginsGateEnabled(t *testing.T) { + t.Setenv(experimentalUIPluginsEnvVar, "1") + cmd := NewUIPluginsCommand() + cmd.SetArgs([]string{}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("expected command to run when gate is enabled, got: %v", err) + } +} + +func TestUIPluginsStubReachableWhenEnabled(t *testing.T) { + t.Setenv(experimentalUIPluginsEnvVar, "1") + cmd := NewUIPluginsCommand() + cmd.SetArgs([]string{"init"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected init stub command to return not implemented error") + } + + if !strings.Contains(err.Error(), "not implemented yet") { + t.Fatalf("expected not implemented error, got: %s", err.Error()) + } +} diff --git a/cmd/ui_plugins/update.go b/cmd/ui_plugins/update.go new file mode 100644 index 00000000..aa7f8ffd --- /dev/null +++ b/cmd/ui_plugins/update.go @@ -0,0 +1,18 @@ +// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved. +package ui_plugins + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newUpdateCommand() *cobra.Command { + return &cobra.Command{ + Use: "update", + Short: "Update a UI plugin manifest configuration", + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("`sail ui-plugins update` is not implemented yet") + }, + } +} diff --git a/cmd/ui_plugins/upload.go b/cmd/ui_plugins/upload.go new file mode 100644 index 00000000..6988b8af --- /dev/null +++ b/cmd/ui_plugins/upload.go @@ -0,0 +1,18 @@ +// Copyright (c) 2026, SailPoint Technologies, Inc. All rights reserved. +package ui_plugins + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newUploadCommand() *cobra.Command { + return &cobra.Command{ + Use: "upload", + Short: "Upload compiled UI plugin assets", + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("`sail ui-plugins upload` is not implemented yet") + }, + } +} From 288a07469fa863a600854b93803402be81049b48 Mon Sep 17 00:00:00 2001 From: Jeremy Gooch Date: Fri, 19 Jun 2026 14:32:23 -0500 Subject: [PATCH 2/2] CSTM-190: Reverting test fixture hunks to avoid noise in PR --- cmd/api/test_data/test_create.json | 2 +- cmd/api/test_data/test_update.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/api/test_data/test_create.json b/cmd/api/test_data/test_create.json index bb058d04..e43c95fc 100644 --- a/cmd/api/test_data/test_create.json +++ b/cmd/api/test_data/test_create.json @@ -1 +1 @@ -{"attributes":{"accountFilter":"!(nativeIdentity.startsWith(\"*DELETED*\"))","accountPropertyFilter":"(groups.containsAll({'Admin'}) || location == 'Austin')","accountReturnFirstLink":false,"accountSortAttribute":"created","accountSortDescending":false,"attributeName":"DEPARTMENT","input":{"attributes":{"attributeName":"first_name","sourceName":"Source"},"type":"accountAttribute"},"requiresPeriodicRefresh":false,"sourceName":"Workday"},"name":"oNYjBXFWwyqnZfqF","type":"dateFormat"} \ No newline at end of file +{"attributes":{"accountFilter":"!(nativeIdentity.startsWith(\"*DELETED*\"))","accountPropertyFilter":"(groups.containsAll({'Admin'}) || location == 'Austin')","accountReturnFirstLink":false,"accountSortAttribute":"created","accountSortDescending":false,"attributeName":"DEPARTMENT","input":{"attributes":{"attributeName":"first_name","sourceName":"Source"},"type":"accountAttribute"},"requiresPeriodicRefresh":false,"sourceName":"Workday"},"name":"PRithHMFRWGzvlna","type":"dateFormat"} \ No newline at end of file diff --git a/cmd/api/test_data/test_update.json b/cmd/api/test_data/test_update.json index 90a5e878..47da2818 100644 --- a/cmd/api/test_data/test_update.json +++ b/cmd/api/test_data/test_update.json @@ -1 +1 @@ -{"attributes":{"accountFilter":"!(nativeIdentity.startsWith(\"*DELETED*\"))","accountPropertyFilter":"(groups.containsAll({'Admin'}) || location == 'Austin')","accountReturnFirstLink":false,"accountSortAttribute":"created","accountSortDescending":false,"attributeName":"UPDATED_DEPARTMENT","input":{"attributes":{"attributeName":"first_name","sourceName":"Source"},"type":"accountAttribute"},"requiresPeriodicRefresh":false,"sourceName":"Workday"},"name":"oNYjBXFWwyqnZfqF","type":"dateFormat"} \ No newline at end of file +{"attributes":{"accountFilter":"!(nativeIdentity.startsWith(\"*DELETED*\"))","accountPropertyFilter":"(groups.containsAll({'Admin'}) || location == 'Austin')","accountReturnFirstLink":false,"accountSortAttribute":"created","accountSortDescending":false,"attributeName":"UPDATED_DEPARTMENT","input":{"attributes":{"attributeName":"first_name","sourceName":"Source"},"type":"accountAttribute"},"requiresPeriodicRefresh":false,"sourceName":"Workday"},"name":"xQTfLofnWtrDsrbX","type":"dateFormat"} \ No newline at end of file