mirror of
https://github.com/LukeHagar/sailpoint-cli.git
synced 2025-12-09 20:57:44 +00:00
Adjusted Project Structure, added environment support, added 90% spconfig functionality
This commit is contained in:
@@ -2,234 +2,33 @@
|
|||||||
package configure
|
package configure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
"errors"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/terminal"
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/tui"
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/types"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func NewConfigureCmd() *cobra.Command {
|
||||||
baseURLTemplate = "https://%s"
|
|
||||||
tokenURLTemplate = "https://%s/oauth/token"
|
|
||||||
authURLTemplate = "https://%s/oauth/authorize"
|
|
||||||
configFolder = ".sailpoint"
|
|
||||||
configYamlFile = "config.yaml"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
baseURL (string)
|
|
||||||
tokenURL (string)
|
|
||||||
authURL (string)
|
|
||||||
)
|
|
||||||
|
|
||||||
func PromptAuth() (string, error) {
|
|
||||||
items := []types.Choice{
|
|
||||||
{Title: "PAT", Description: "Person Access Token - Single User"},
|
|
||||||
{Title: "OAuth", Description: "OAuth2.0 Authentication - Sign in via the Website"},
|
|
||||||
}
|
|
||||||
|
|
||||||
choice, err := tui.PromptList(items, "Choose an authentication method to configure")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.ToLower(choice.Title), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConfigureCmd(client client.Client) *cobra.Command {
|
|
||||||
var debug bool
|
var debug bool
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "configure",
|
Use: "configure",
|
||||||
Short: "configure authentication for the cli",
|
Short: "configure pat authentication for the currently active environment",
|
||||||
Long: "\nConfigure Authentication for the CLI\nSupported Methods: (PAT, OAuth)\n\nPrerequisites:\n\nPAT:\n Tenant\n Client ID\n Client Secret\n\nOAuth:\n Tenant\n Client ID\n Client Secret - Optional Depending on configuration\n Callback Port (ex. http://localhost:{3000}/callback)\n Callback Path (ex. http://localhost:3000{/callback})",
|
Long: "\nConfigure PAT Authentication for the CLI\n\nPrerequisites:\n\nClient ID\nClient Secret\n",
|
||||||
Aliases: []string{"conf"},
|
Aliases: []string{"conf"},
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
viper.Set("debug", debug)
|
||||||
|
|
||||||
var AuthType string
|
ClientID := terminal.InputPrompt("Personal Access Token Client ID:")
|
||||||
var err error
|
ClientSecret := terminal.InputPrompt("Personal Access Token Client Secret:")
|
||||||
|
|
||||||
if len(args) > 0 {
|
config.SetPatClientID(ClientID)
|
||||||
AuthType = args[0]
|
config.SetPatClientSecret(ClientSecret)
|
||||||
} else {
|
|
||||||
AuthType, err = PromptAuth()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := getConfigureParamsFromStdin(AuthType, debug)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = updateConfigFile(config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch strings.ToLower(AuthType) {
|
|
||||||
case "pat":
|
|
||||||
err = config.PATLogin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "oauth":
|
|
||||||
err := config.OAuthLogin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return errors.New("invalid authtype")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().BoolVarP(&debug, "Debug", "d", false, "Specifies if the debug flag should be set")
|
cmd.Flags().BoolVarP(&debug, "debug", "d", false, "Specifies if the debug flag should be set")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateConfigFile(conf types.CLIConfig) error {
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(filepath.Join(home, configFolder)); os.IsNotExist(err) {
|
|
||||||
err = os.Mkdir(filepath.Join(home, configFolder), 0777)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to create %s folder for config. %v", configFolder, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viper.Set("authtype", conf.AuthType)
|
|
||||||
viper.Set("debug", conf.Debug)
|
|
||||||
|
|
||||||
viper.Set(fmt.Sprintf("environments.%s", conf.ActiveEnvironment), conf.Environments[conf.ActiveEnvironment])
|
|
||||||
|
|
||||||
err = viper.WriteConfig()
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
|
||||||
err = viper.SafeWriteConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setIDNUrls(tenant string) {
|
|
||||||
var tokens = strings.Split(tenant, ".")
|
|
||||||
tokens = append(tokens[:1+1], tokens[1:]...)
|
|
||||||
tokens[1] = "api"
|
|
||||||
var api_base = strings.Join(tokens, ".")
|
|
||||||
baseURL = fmt.Sprintf(baseURLTemplate, api_base)
|
|
||||||
tokenURL = fmt.Sprintf(tokenURLTemplate, api_base)
|
|
||||||
authURL = fmt.Sprintf(authURLTemplate, tenant)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getConfigureParamsFromStdin(AuthType string, debug bool) (types.CLIConfig, error) {
|
|
||||||
var config types.CLIConfig
|
|
||||||
|
|
||||||
switch strings.ToLower(AuthType) {
|
|
||||||
case "pat":
|
|
||||||
var Pat types.PatConfig
|
|
||||||
paramsNames := []string{
|
|
||||||
"Tenant (ex. tenant.identitynow.com): ",
|
|
||||||
"Personal Access Token Client ID: ",
|
|
||||||
"Personal Access Token Client Secret: ",
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
for _, pm := range paramsNames {
|
|
||||||
fmt.Print(pm)
|
|
||||||
scanner.Scan()
|
|
||||||
value := scanner.Text()
|
|
||||||
|
|
||||||
if value == "" {
|
|
||||||
return config, fmt.Errorf("%s parameter is empty", pm[:len(pm)-2])
|
|
||||||
}
|
|
||||||
|
|
||||||
switch pm {
|
|
||||||
case paramsNames[0]:
|
|
||||||
setIDNUrls(value)
|
|
||||||
Pat.Tenant = value
|
|
||||||
Pat.BaseUrl = baseURL
|
|
||||||
Pat.TokenUrl = tokenURL
|
|
||||||
case paramsNames[1]:
|
|
||||||
Pat.ClientID = value
|
|
||||||
case paramsNames[2]:
|
|
||||||
Pat.ClientSecret = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.AuthType = AuthType
|
|
||||||
tempEnv := config.Environments[config.ActiveEnvironment]
|
|
||||||
tempEnv.Pat = Pat
|
|
||||||
config.Environments[config.ActiveEnvironment] = tempEnv
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
case "oauth":
|
|
||||||
var OAuth types.OAuthConfig
|
|
||||||
paramsNames := []string{
|
|
||||||
"Tenant (ex. tenant.identitynow.com): ",
|
|
||||||
"OAuth Client ID: ",
|
|
||||||
"OAuth Client Secret: ",
|
|
||||||
"OAuth Redirect Port (ex. http://localhost:{3000}/callback): ",
|
|
||||||
"OAuth Redirect Path (ex. http://localhost:3000{/callback}): ",
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
for _, pm := range paramsNames {
|
|
||||||
fmt.Print(pm)
|
|
||||||
scanner.Scan()
|
|
||||||
value := scanner.Text()
|
|
||||||
|
|
||||||
if value == "" && pm != paramsNames[2] {
|
|
||||||
return config, fmt.Errorf("%s parameter is empty", pm[:len(pm)-2])
|
|
||||||
}
|
|
||||||
|
|
||||||
switch pm {
|
|
||||||
case paramsNames[0]:
|
|
||||||
setIDNUrls(value)
|
|
||||||
OAuth.Tenant = value
|
|
||||||
OAuth.BaseUrl = baseURL
|
|
||||||
OAuth.TokenUrl = tokenURL
|
|
||||||
OAuth.AuthUrl = authURL
|
|
||||||
case paramsNames[1]:
|
|
||||||
OAuth.ClientID = value
|
|
||||||
case paramsNames[2]:
|
|
||||||
OAuth.ClientSecret = value
|
|
||||||
case paramsNames[3]:
|
|
||||||
OAuth.Redirect.Port, _ = strconv.Atoi(value)
|
|
||||||
case paramsNames[4]:
|
|
||||||
OAuth.Redirect.Path = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.AuthType = AuthType
|
|
||||||
tempEnv := config.Environments[config.ActiveEnvironment]
|
|
||||||
tempEnv.OAuth = OAuth
|
|
||||||
config.Environments[config.ActiveEnvironment] = tempEnv
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
default:
|
|
||||||
return config, errors.New("invalid auth type provided")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
@@ -17,7 +18,7 @@ const (
|
|||||||
connectorsEndpoint = "/beta/platform-connectors"
|
connectorsEndpoint = "/beta/platform-connectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewConnCmd(client client.Client) *cobra.Command {
|
func NewConnCmd() *cobra.Command {
|
||||||
conn := &cobra.Command{
|
conn := &cobra.Command{
|
||||||
Use: "connectors",
|
Use: "connectors",
|
||||||
Short: "manage connectors",
|
Short: "manage connectors",
|
||||||
@@ -27,23 +28,30 @@ func NewConnCmd(client client.Client) *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Config, err := config.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Client := client.NewSpClient(Config)
|
||||||
|
|
||||||
conn.PersistentFlags().StringP("conn-endpoint", "e", connectorsEndpoint, "Override connectors endpoint")
|
conn.PersistentFlags().StringP("conn-endpoint", "e", connectorsEndpoint, "Override connectors endpoint")
|
||||||
|
|
||||||
conn.AddCommand(
|
conn.AddCommand(
|
||||||
newConnInitCmd(),
|
newConnInitCmd(),
|
||||||
newConnListCmd(client),
|
newConnListCmd(Client),
|
||||||
newConnGetCmd(client),
|
newConnGetCmd(Client),
|
||||||
newConnUpdateCmd(client),
|
newConnUpdateCmd(Client),
|
||||||
newConnCreateCmd(client),
|
newConnCreateCmd(Client),
|
||||||
newConnCreateVersionCmd(client),
|
newConnCreateVersionCmd(Client),
|
||||||
newConnVersionsCmd(client),
|
newConnVersionsCmd(Client),
|
||||||
newConnInvokeCmd(client),
|
newConnInvokeCmd(Client),
|
||||||
newConnValidateCmd(client),
|
newConnValidateCmd(Client),
|
||||||
newConnTagCmd(client),
|
newConnTagCmd(Client),
|
||||||
newConnValidateSourcesCmd(client),
|
newConnValidateSourcesCmd(Client),
|
||||||
newConnLogsCmd(client),
|
newConnLogsCmd(Client),
|
||||||
newConnStatsCmd(client),
|
newConnStatsCmd(Client),
|
||||||
newConnDeleteCmd(client),
|
newConnDeleteCmd(Client),
|
||||||
)
|
)
|
||||||
|
|
||||||
return conn
|
return conn
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/terminal"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ func newConnInvokeChangePasswordCmd(spClient client.Client) *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
password, err := util.PromptPassword("Enter Password:")
|
password, err := terminal.PromptPassword("Enter Password:")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/mocks"
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ func TestNewConnCmd_noArgs(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
cmd := NewConnCmd(mocks.NewMockClient(ctrl))
|
cmd := NewConnCmd()
|
||||||
if len(cmd.Commands()) != numConnSubcommands {
|
if len(cmd.Commands()) != numConnSubcommands {
|
||||||
t.Fatalf("expected: %d, actual: %d", len(cmd.Commands()), numConnSubcommands)
|
t.Fatalf("expected: %d, actual: %d", len(cmd.Commands()), numConnSubcommands)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
package root
|
package root
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/configure"
|
"github.com/fatih/color"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/tui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func PromptAuth() (string, error) {
|
||||||
|
items := []tui.Choice{
|
||||||
|
{Title: "PAT", Description: "Person Access Token - Single User"},
|
||||||
|
{Title: "OAuth", Description: "OAuth2.0 Authentication - Sign in via the Web Portal"},
|
||||||
|
}
|
||||||
|
|
||||||
|
choice, err := tui.PromptList(items, "Choose an authentication method to configure")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToLower(choice.Title), nil
|
||||||
|
}
|
||||||
|
|
||||||
func newAuthCommand() *cobra.Command {
|
func newAuthCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "auth",
|
Use: "auth",
|
||||||
@@ -17,12 +32,14 @@ func newAuthCommand() *cobra.Command {
|
|||||||
Example: "sail auth pat | oauth",
|
Example: "sail auth pat | oauth",
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
var selection string
|
var selection string
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
selection = args[0]
|
selection = args[0]
|
||||||
} else {
|
} else {
|
||||||
selection, err = configure.PromptAuth()
|
selection, err = PromptAuth()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -30,23 +47,17 @@ func newAuthCommand() *cobra.Command {
|
|||||||
|
|
||||||
switch strings.ToLower(selection) {
|
switch strings.ToLower(selection) {
|
||||||
case "pat":
|
case "pat":
|
||||||
viper.Set("authtype", "pat")
|
if config.GetAuthType() != "pat" {
|
||||||
case "oauth":
|
config.SetAuthType("pat")
|
||||||
viper.Set("authtype", "oauth")
|
color.Blue("authentication method set to pat")
|
||||||
default:
|
|
||||||
return errors.New("invalid selection")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = viper.WriteConfig()
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
|
||||||
err = viper.SafeWriteConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
case "oauth":
|
||||||
|
if config.GetAuthType() != "oauth" {
|
||||||
|
config.SetAuthType("oauth")
|
||||||
|
color.Blue("authentication method set to oauth")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid selection")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -23,18 +23,6 @@ func newDebugCommand() *cobra.Command {
|
|||||||
viper.Set("debug", false)
|
viper.Set("debug", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := viper.WriteConfig()
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
|
||||||
err = viper.SafeWriteConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,19 @@ package root
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/configure"
|
"github.com/sailpoint-oss/sailpoint-cli/cmd/configure"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/connector"
|
"github.com/sailpoint-oss/sailpoint-cli/cmd/connector"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/cmd/environment"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/search"
|
"github.com/sailpoint-oss/sailpoint-cli/cmd/search"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/spconfig"
|
"github.com/sailpoint-oss/sailpoint-cli/cmd/spconfig"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/transform"
|
"github.com/sailpoint-oss/sailpoint-cli/cmd/transform"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/va"
|
"github.com/sailpoint-oss/sailpoint-cli/cmd/va"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "0.4.1"
|
var version = "0.4.2"
|
||||||
|
|
||||||
func NewRootCmd(client client.Client, apiClient *sailpoint.APIClient) *cobra.Command {
|
func NewRootCmd() *cobra.Command {
|
||||||
root := &cobra.Command{
|
root := &cobra.Command{
|
||||||
Use: "sail",
|
Use: "sail",
|
||||||
Short: "The SailPoint CLI allows you to administer your IdentityNow tenant from the command line.\n\nNavigate to developer.sailpoint.com to learn more.",
|
Short: "The SailPoint CLI allows you to administer your IdentityNow tenant from the command line.\n\nNavigate to developer.sailpoint.com to learn more.",
|
||||||
@@ -35,12 +34,13 @@ func NewRootCmd(client client.Client, apiClient *sailpoint.APIClient) *cobra.Com
|
|||||||
root.AddCommand(
|
root.AddCommand(
|
||||||
newDebugCommand(),
|
newDebugCommand(),
|
||||||
newAuthCommand(),
|
newAuthCommand(),
|
||||||
configure.NewConfigureCmd(client),
|
environment.NewEnvironmentCommand(),
|
||||||
connector.NewConnCmd(client),
|
configure.NewConfigureCmd(),
|
||||||
transform.NewTransformCmd(client),
|
connector.NewConnCmd(),
|
||||||
|
transform.NewTransformCmd(),
|
||||||
va.NewVACmd(),
|
va.NewVACmd(),
|
||||||
search.NewSearchCmd(apiClient),
|
search.NewSearchCmd(),
|
||||||
spconfig.NewSPConfigCmd(apiClient),
|
spconfig.NewSPConfigCmd(),
|
||||||
)
|
)
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/mocks"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Expected number of subcommands to `sp` root command
|
// Expected number of subcommands to `sp` root command
|
||||||
@@ -18,7 +17,7 @@ func TestNewRootCmd_noArgs(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
cmd := NewRootCmd(mocks.NewMockClient(ctrl), nil)
|
cmd := NewRootCmd()
|
||||||
if len(cmd.Commands()) != numRootSubcommands {
|
if len(cmd.Commands()) != numRootSubcommands {
|
||||||
t.Fatalf("expected: %d, actual: %d", len(cmd.Commands()), numRootSubcommands)
|
t.Fatalf("expected: %d, actual: %d", len(cmd.Commands()), numRootSubcommands)
|
||||||
}
|
}
|
||||||
@@ -46,7 +45,7 @@ func TestNewRootCmd_completionDisabled(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
cmd := NewRootCmd(mocks.NewMockClient(ctrl), nil)
|
cmd := NewRootCmd()
|
||||||
|
|
||||||
b := new(bytes.Buffer)
|
b := new(bytes.Buffer)
|
||||||
cmd.SetOut(b)
|
cmd.SetOut(b)
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/search"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newQueryCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
func newQueryCmd() *cobra.Command {
|
||||||
var output string
|
var folderPath string
|
||||||
var indicies []string
|
var indicies []string
|
||||||
|
var outputTypes []string
|
||||||
var sort []string
|
var sort []string
|
||||||
var searchQuery string
|
var searchQuery string
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -24,28 +25,28 @@ func newQueryCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
if output == "" {
|
apiClient := config.InitAPIClient()
|
||||||
output = "search_results"
|
|
||||||
|
if folderPath == "" {
|
||||||
|
folderPath = "search_results"
|
||||||
}
|
}
|
||||||
|
|
||||||
searchQuery = args[0]
|
searchQuery = args[0]
|
||||||
fmt.Println(searchQuery)
|
fmt.Println(searchQuery)
|
||||||
|
|
||||||
search, err := util.BuildSearch(searchQuery, sort, indicies)
|
searchObj, err := search.BuildSearch(searchQuery, sort, indicies)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
color.Blue("\nPerforming Search\nQuery: \"%s\"\nIndicie: %s\n", searchQuery, indicies)
|
color.Blue("\nPerforming Search\nQuery: \"%s\"\nIndicie: %s\n", searchQuery, indicies)
|
||||||
|
|
||||||
formattedResponse, err := util.PerformSearch(*apiClient, search)
|
formattedResponse, err := search.PerformSearch(*apiClient, searchObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := fmt.Sprintf("query=%s&indicie=%s.json", searchQuery, indicies)
|
err = search.IterateIndicies(formattedResponse, searchQuery, folderPath, outputTypes)
|
||||||
|
|
||||||
err = util.SaveResults(formattedResponse, fileName, output)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -56,7 +57,8 @@ func newQueryCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
|
|
||||||
cmd.Flags().StringArrayVarP(&indicies, "indicies", "i", []string{}, "indicies to perform the search query on")
|
cmd.Flags().StringArrayVarP(&indicies, "indicies", "i", []string{}, "indicies to perform the search query on")
|
||||||
cmd.Flags().StringArrayVarP(&sort, "sort", "s", []string{}, "the sort value for the api call (examples)")
|
cmd.Flags().StringArrayVarP(&sort, "sort", "s", []string{}, "the sort value for the api call (examples)")
|
||||||
cmd.Flags().StringVarP(&output, "output", "o", "", "path to save the search results in. If the directory doesn't exist, then it will be automatically created. (default is the current working directory)")
|
cmd.Flags().StringArrayVarP(&outputTypes, "output types", "o", []string{"json"}, "the sort value for the api call (examples)")
|
||||||
|
cmd.Flags().StringVarP(&folderPath, "folderPath", "f", "", "folder path to save the search results in. If the directory doesn't exist, then it will be automatically created. (default is the current working directory)")
|
||||||
|
|
||||||
cmd.MarkFlagRequired("indicies")
|
cmd.MarkFlagRequired("indicies")
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ package search
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewSearchCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
func NewSearchCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "search",
|
Use: "search",
|
||||||
Short: "perform search in identitynow with a search string",
|
Short: "perform search in identitynow with a search string",
|
||||||
@@ -22,8 +21,8 @@ func NewSearchCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newQueryCmd(apiClient),
|
newQueryCmd(),
|
||||||
newTemplateCmd(apiClient),
|
newTemplateCmd(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
@@ -7,14 +7,17 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/search"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/templates"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/terminal"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/types"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/types"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTemplateCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
func newTemplateCmd() *cobra.Command {
|
||||||
var output string
|
var outputTypes []string
|
||||||
|
var folderPath string
|
||||||
var template string
|
var template string
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "template",
|
Use: "template",
|
||||||
@@ -25,12 +28,10 @@ func newTemplateCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
if output == "" {
|
apiClient := config.InitAPIClient()
|
||||||
output = "search_results"
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedTemplate types.SearchTemplate
|
var selectedTemplate templates.SearchTemplate
|
||||||
searchTemplates, err := util.GetSearchTemplates()
|
searchTemplates, err := templates.GetSearchTemplates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -38,26 +39,7 @@ func newTemplateCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
template = args[0]
|
template = args[0]
|
||||||
} else {
|
} else {
|
||||||
// var prompts []types.Choice
|
template, err = templates.SelectTemplate(searchTemplates)
|
||||||
// for i := 0; i < len(searchTemplates); i++ {
|
|
||||||
// temp := searchTemplates[i]
|
|
||||||
|
|
||||||
// var description string
|
|
||||||
// if len(temp.Variables) > 0 {
|
|
||||||
// description = fmt.Sprintf("%s - Accepts Input", temp.Description)
|
|
||||||
// } else {
|
|
||||||
// description = temp.Description
|
|
||||||
// }
|
|
||||||
// prompts = append(prompts, types.Choice{Title: temp.Name, Description: description})
|
|
||||||
// }
|
|
||||||
|
|
||||||
// intermediate, err := tui.PromptList(prompts, "Select a Template")
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// template = intermediate.Title
|
|
||||||
|
|
||||||
template, err = util.SelectTemplate(searchTemplates)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -68,18 +50,18 @@ func newTemplateCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
|
|
||||||
color.Blue("Selected Template: %s\n", template)
|
color.Blue("Selected Template: %s\n", template)
|
||||||
|
|
||||||
matches := types.Filter(searchTemplates, func(st types.SearchTemplate) bool { return st.Name == template })
|
matches := types.Filter(searchTemplates, func(st templates.SearchTemplate) bool { return st.Name == template })
|
||||||
if len(matches) < 1 {
|
if len(matches) < 1 {
|
||||||
return fmt.Errorf("no template matches for %s", template)
|
return fmt.Errorf("no template matches for %s", template)
|
||||||
} else if len(matches) > 1 {
|
} else if len(matches) > 1 {
|
||||||
color.Yellow("multiple template matches for %s", template)
|
color.Yellow("multiple template matches for %s", template)
|
||||||
}
|
}
|
||||||
selectedTemplate = types.Filter(searchTemplates, func(st types.SearchTemplate) bool { return st.Name == template })[0]
|
selectedTemplate = matches[0]
|
||||||
varCount := len(selectedTemplate.Variables)
|
varCount := len(selectedTemplate.Variables)
|
||||||
if varCount > 0 {
|
if varCount > 0 {
|
||||||
for i := 0; i < varCount; i++ {
|
for i := 0; i < varCount; i++ {
|
||||||
varEntry := selectedTemplate.Variables[i]
|
varEntry := selectedTemplate.Variables[i]
|
||||||
resp := util.InputPrompt(fmt.Sprintf("Input %s:", varEntry.Prompt))
|
resp := terminal.InputPrompt(fmt.Sprintf("Input %s:", varEntry.Prompt))
|
||||||
selectedTemplate.Raw = []byte(strings.ReplaceAll(string(selectedTemplate.Raw), fmt.Sprintf("{{%s}}", varEntry.Name), resp))
|
selectedTemplate.Raw = []byte(strings.ReplaceAll(string(selectedTemplate.Raw), fmt.Sprintf("{{%s}}", varEntry.Name), resp))
|
||||||
}
|
}
|
||||||
err := json.Unmarshal(selectedTemplate.Raw, &selectedTemplate.SearchQuery)
|
err := json.Unmarshal(selectedTemplate.Raw, &selectedTemplate.SearchQuery)
|
||||||
@@ -90,14 +72,12 @@ func newTemplateCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
|
|
||||||
color.Blue("\nPerforming Search\nQuery: \"%s\"\nIndicie: %s\n\n", selectedTemplate.SearchQuery.Query.GetQuery(), selectedTemplate.SearchQuery.Indices)
|
color.Blue("\nPerforming Search\nQuery: \"%s\"\nIndicie: %s\n\n", selectedTemplate.SearchQuery.Query.GetQuery(), selectedTemplate.SearchQuery.Indices)
|
||||||
|
|
||||||
formattedResponse, err := util.PerformSearch(*apiClient, selectedTemplate.SearchQuery)
|
formattedResponse, err := search.PerformSearch(*apiClient, selectedTemplate.SearchQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := fmt.Sprintf("query=%s&indicie=%s.json", selectedTemplate.SearchQuery.Query.GetQuery(), selectedTemplate.SearchQuery.Indices)
|
err = search.IterateIndicies(formattedResponse, selectedTemplate.SearchQuery.Query.GetQuery(), folderPath, outputTypes)
|
||||||
|
|
||||||
err = util.SaveResults(formattedResponse, fileName, output)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -106,7 +86,8 @@ func newTemplateCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&output, "output", "o", "", "path to save the search results in. If the directory doesn't exist, then it will be automatically created. (default is the current working directory)")
|
cmd.Flags().StringArrayVarP(&outputTypes, "output types", "o", []string{"json"}, "the sort value for the api call (examples)")
|
||||||
|
cmd.Flags().StringVarP(&folderPath, "folderPath", "f", "search_results", "folder path to save the search results in. If the directory doesn't exist, then it will be automatically created. (default is the current working directory)")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
37
cmd/spconfig/download.go
Normal file
37
cmd/spconfig/download.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
|
||||||
|
package spconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/spconfig"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newDownloadCmd() *cobra.Command {
|
||||||
|
var folderPath string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "download",
|
||||||
|
Short: "download results of an export job from identitynow",
|
||||||
|
Long: "download results of an export job from identitynow",
|
||||||
|
Example: "sail spconfig download 37a64554-bf83-4d6a-8303-e6492251806b",
|
||||||
|
Aliases: []string{"que"},
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
jobId := args[i]
|
||||||
|
color.Blue("Checking Export Job: %s", jobId)
|
||||||
|
err := spconfig.DownloadExport(jobId, "spconfig-export-"+jobId+".json", folderPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&folderPath, "folderPath", "f", "spconfig-exports", "folder path to save the search results in. If the directory doesn't exist, then it will be automatically created. (default is the current working directory)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -5,17 +5,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
|
||||||
sailpointbetasdk "github.com/sailpoint-oss/golang-sdk/sdk-output/beta"
|
sailpointbetasdk "github.com/sailpoint-oss/golang-sdk/sdk-output/beta"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/spconfig"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newExportCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
func newExportCmd() *cobra.Command {
|
||||||
|
var folderPath string
|
||||||
var description string
|
var description string
|
||||||
var includeTypes []string
|
var includeTypes []string
|
||||||
var excludeTypes []string
|
var excludeTypes []string
|
||||||
var exportAll bool
|
var wait bool
|
||||||
var payload *sailpointbetasdk.ExportPayload
|
var payload *sailpointbetasdk.ExportPayload
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "export",
|
Use: "export",
|
||||||
@@ -26,6 +27,8 @@ func newExportCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
apiClient := config.InitAPIClient()
|
||||||
|
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
|
|
||||||
payload = sailpointbetasdk.NewExportPayload()
|
payload = sailpointbetasdk.NewExportPayload()
|
||||||
@@ -33,22 +36,27 @@ func newExportCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
payload.IncludeTypes = includeTypes
|
payload.IncludeTypes = includeTypes
|
||||||
payload.ExcludeTypes = excludeTypes
|
payload.ExcludeTypes = excludeTypes
|
||||||
|
|
||||||
fmt.Println(payload.GetIncludeTypes())
|
|
||||||
|
|
||||||
job, _, err := apiClient.Beta.SPConfigApi.SpConfigExport(ctx).ExportPayload(*payload).Execute()
|
job, _, err := apiClient.Beta.SPConfigApi.SpConfigExport(ctx).ExportPayload(*payload).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
util.PrintJob(*job)
|
spconfig.PrintJob(*job)
|
||||||
|
|
||||||
|
if wait {
|
||||||
|
fmt.Println("waiting for export task to complete")
|
||||||
|
spconfig.DownloadExport(job.JobId, "spconfig-export-"+job.JobId+".json", folderPath)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&folderPath, "folderPath", "f", "spconfig-exports", "folder path to save the search results in. If the directory doesn't exist, then it will be automatically created. (default is the current working directory)")
|
||||||
cmd.Flags().StringVarP(&description, "description", "d", "", "optional description for the export job")
|
cmd.Flags().StringVarP(&description, "description", "d", "", "optional description for the export job")
|
||||||
cmd.Flags().BoolVarP(&exportAll, "export all", "a", false, "optional flag to export all items")
|
|
||||||
cmd.Flags().StringArrayVarP(&includeTypes, "include types", "i", []string{}, "types to include in export job")
|
cmd.Flags().StringArrayVarP(&includeTypes, "include types", "i", []string{}, "types to include in export job")
|
||||||
cmd.Flags().StringArrayVarP(&excludeTypes, "exclude types", "e", []string{}, "types to exclude in export job")
|
cmd.Flags().StringArrayVarP(&excludeTypes, "exclude types", "e", []string{}, "types to exclude in export job")
|
||||||
|
cmd.Flags().BoolVarP(&wait, "wait", "w", false, "wait for the export job to finish, and download the results")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
42
cmd/spconfig/import.go
Normal file
42
cmd/spconfig/import.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
|
||||||
|
package spconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
sailpointbetasdk "github.com/sailpoint-oss/golang-sdk/sdk-output/beta"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/spconfig"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newImportCommand() *cobra.Command {
|
||||||
|
var payload *sailpointbetasdk.ImportOptions
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "import",
|
||||||
|
Short: "begin an import job in identitynow",
|
||||||
|
Long: "initiate an import job in identitynow",
|
||||||
|
Example: "sail spconfig import",
|
||||||
|
Aliases: []string{"que"},
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
apiClient := config.InitAPIClient()
|
||||||
|
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
|
payload = sailpointbetasdk.NewImportOptions()
|
||||||
|
|
||||||
|
job, _, err := apiClient.Beta.SPConfigApi.SpConfigImport(ctx).Data(args[0]).Options(*payload).Execute()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
spconfig.PrintJob(*job)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -4,11 +4,10 @@ package spconfig
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewSPConfigCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
func NewSPConfigCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "spconfig",
|
Use: "spconfig",
|
||||||
Short: "perform spconfig operations in identitynow",
|
Short: "perform spconfig operations in identitynow",
|
||||||
@@ -22,8 +21,11 @@ func NewSPConfigCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newExportCmd(apiClient),
|
newExportCmd(),
|
||||||
newExportStatusCmd(apiClient),
|
newExportStatusCmd(),
|
||||||
|
newTemplateCmd(),
|
||||||
|
newDownloadCmd(),
|
||||||
|
newImportCommand(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ package spconfig
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/spconfig"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newExportStatusCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
func newExportStatusCmd() *cobra.Command {
|
||||||
var exportJobs []string
|
var exportJobs []string
|
||||||
var importJobs []string
|
var importJobs []string
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -21,6 +21,8 @@ func newExportStatusCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
apiClient := config.InitAPIClient()
|
||||||
|
|
||||||
for i := 0; i < len(exportJobs); i++ {
|
for i := 0; i < len(exportJobs); i++ {
|
||||||
job := exportJobs[i]
|
job := exportJobs[i]
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
@@ -29,7 +31,7 @@ func newExportStatusCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
util.PrintJob(*status)
|
spconfig.PrintJob(*status)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(importJobs); i++ {
|
for i := 0; i < len(importJobs); i++ {
|
||||||
@@ -40,7 +42,7 @@ func newExportStatusCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
util.PrintJob(*status)
|
spconfig.PrintJob(*status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
100
cmd/spconfig/template.go
Normal file
100
cmd/spconfig/template.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// Copyright (c) 2021, SailPoint Technologies, Inc. All rights reserved.
|
||||||
|
package spconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/spconfig"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/templates"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/terminal"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/types"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTemplateCmd() *cobra.Command {
|
||||||
|
var outputTypes []string
|
||||||
|
var folderPath string
|
||||||
|
var template string
|
||||||
|
var wait bool
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "template",
|
||||||
|
Short: "begin an export task using a template",
|
||||||
|
Long: "begin an export task in IdentityNow using a template",
|
||||||
|
Example: "sail spconfig template",
|
||||||
|
Aliases: []string{"temp"},
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
apiClient := config.InitAPIClient()
|
||||||
|
|
||||||
|
if folderPath == "" {
|
||||||
|
folderPath = "search_results"
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedTemplate templates.ExportTemplate
|
||||||
|
exportTemplates, err := templates.GetExportTemplates()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
template = args[0]
|
||||||
|
} else {
|
||||||
|
template, err = templates.SelectTemplate(exportTemplates)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if template == "" {
|
||||||
|
return fmt.Errorf("no template specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
color.Blue("Selected Template: %s\n", template)
|
||||||
|
|
||||||
|
matches := types.Filter(exportTemplates, func(st templates.ExportTemplate) bool { return st.Name == template })
|
||||||
|
if len(matches) < 1 {
|
||||||
|
return fmt.Errorf("no template matches for %s", template)
|
||||||
|
} else if len(matches) > 1 {
|
||||||
|
color.Yellow("multiple template matches for %s", template)
|
||||||
|
}
|
||||||
|
selectedTemplate = matches[0]
|
||||||
|
varCount := len(selectedTemplate.Variables)
|
||||||
|
if varCount > 0 {
|
||||||
|
for i := 0; i < varCount; i++ {
|
||||||
|
varEntry := selectedTemplate.Variables[i]
|
||||||
|
resp := terminal.InputPrompt(fmt.Sprintf("Input %s:", varEntry.Prompt))
|
||||||
|
selectedTemplate.Raw = []byte(strings.ReplaceAll(string(selectedTemplate.Raw), fmt.Sprintf("{{%s}}", varEntry.Name), resp))
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(selectedTemplate.Raw, &selectedTemplate.ExportBody)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
job, _, err := apiClient.Beta.SPConfigApi.SpConfigExport(context.TODO()).ExportPayload(selectedTemplate.ExportBody).Execute()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
spconfig.PrintJob(*job)
|
||||||
|
|
||||||
|
if wait {
|
||||||
|
color.Blue("Checking Export Job: %s", job.JobId)
|
||||||
|
spconfig.DownloadExport(job.JobId, "spconfig-export-"+template+job.JobId+".json", folderPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringArrayVarP(&outputTypes, "output types", "o", []string{"json"}, "the sort value for the api call (examples)")
|
||||||
|
cmd.Flags().StringVarP(&folderPath, "folderPath", "f", "spconfig-exports", "folder path to save the search results in. If the directory doesn't exist, then it will be automatically created. (default is the current working directory)")
|
||||||
|
cmd.Flags().BoolVarP(&wait, "wait", "w", false, "wait for the export job to finish, and download the results")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -2,20 +2,18 @@
|
|||||||
package transform
|
package transform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
sailpointsdk "github.com/sailpoint-oss/golang-sdk/sdk-output/v3"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newCreateCmd(client client.Client) *cobra.Command {
|
func newCreateCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "create",
|
Use: "create",
|
||||||
Short: "create transform",
|
Short: "create transform",
|
||||||
@@ -49,26 +47,15 @@ func newCreateCmd(client client.Client) *cobra.Command {
|
|||||||
return fmt.Errorf("the transform must have a name")
|
return fmt.Errorf("the transform must have a name")
|
||||||
}
|
}
|
||||||
|
|
||||||
raw, err := json.Marshal(data)
|
transform := sailpointsdk.NewTransform(data["name"].(string), data["type"].(string), data["attributes"].(map[string]interface{}))
|
||||||
|
|
||||||
|
apiClient := config.InitAPIClient()
|
||||||
|
_, _, err := apiClient.V3.TransformsApi.CreateTransform(context.TODO()).Transform(*transform).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint := cmd.Flags().Lookup("transforms-endpoint").Value.String()
|
err = ListTransforms()
|
||||||
resp, err := client.Post(cmd.Context(), util.ResourceUrl(endpoint), "application/json", bytes.NewReader(raw))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
_ = Body.Close()
|
|
||||||
}(resp.Body)
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
return fmt.Errorf("create transform failed. status: %s\nbody: %s", resp.Status, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = listTransforms(client, endpoint, cmd)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func TestNewCreateCmd(t *testing.T) {
|
|||||||
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
|
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
|
||||||
Times(1)
|
Times(1)
|
||||||
|
|
||||||
cmd := newCreateCmd(client)
|
cmd := newCreateCmd()
|
||||||
|
|
||||||
b := new(bytes.Buffer)
|
b := new(bytes.Buffer)
|
||||||
cmd.SetOut(b)
|
cmd.SetOut(b)
|
||||||
|
|||||||
@@ -2,23 +2,20 @@
|
|||||||
package transform
|
package transform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/table"
|
"github.com/charmbracelet/bubbles/table"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
tuitable "github.com/sailpoint-oss/sailpoint-cli/internal/tui/table"
|
tuitable "github.com/sailpoint-oss/sailpoint-cli/internal/tui/table"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDeleteCmd(client client.Client) *cobra.Command {
|
func newDeleteCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "delete [TRANSFORM-ID]",
|
Use: "delete [TRANSFORM-ID]",
|
||||||
Short: "delete transform",
|
Short: "delete transform",
|
||||||
@@ -28,17 +25,13 @@ func newDeleteCmd(client client.Client) *cobra.Command {
|
|||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
endpoint := cmd.Flags().Lookup("transforms-endpoint").Value.String()
|
var id string
|
||||||
|
|
||||||
id := ""
|
|
||||||
|
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
id = args[0]
|
id = args[0]
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if id == "" {
|
transforms, err := GetTransforms()
|
||||||
|
|
||||||
transforms, err := getTransforms(client, endpoint, cmd)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -52,7 +45,8 @@ func newDeleteCmd(client client.Client) *cobra.Command {
|
|||||||
var rows []table.Row
|
var rows []table.Row
|
||||||
|
|
||||||
for i := 0; i < len(transforms); i++ {
|
for i := 0; i < len(transforms); i++ {
|
||||||
rows = append(rows, transforms[i].TransformToRows())
|
transform := transforms[i]
|
||||||
|
rows = append(rows, []string{*transform.Id, transform.Name})
|
||||||
}
|
}
|
||||||
|
|
||||||
t := table.New(
|
t := table.New(
|
||||||
@@ -85,25 +79,18 @@ func newDeleteCmd(client client.Client) *cobra.Command {
|
|||||||
if len(tempRow) > 0 {
|
if len(tempRow) > 0 {
|
||||||
id = m.Retrieve()[1]
|
id = m.Retrieve()[1]
|
||||||
} else {
|
} else {
|
||||||
return errors.New("no transform selected")
|
return fmt.Errorf("no transform selected")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Delete(cmd.Context(), util.ResourceUrl(endpoint, id), nil)
|
apiClient := config.InitAPIClient()
|
||||||
|
_, err := apiClient.V3.TransformsApi.DeleteTransform(context.TODO(), id).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
_ = Body.Close()
|
|
||||||
}(resp.Body)
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusNoContent {
|
err = ListTransforms()
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
return fmt.Errorf("delete transform failed. status: %s\nbody: %s", resp.Status, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = listTransforms(client, endpoint, cmd)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func TestNewDeleteCmd(t *testing.T) {
|
|||||||
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
|
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
|
||||||
Times(1)
|
Times(1)
|
||||||
|
|
||||||
cmd := newDeleteCmd(client)
|
cmd := newDeleteCmd()
|
||||||
|
|
||||||
b := new(bytes.Buffer)
|
b := new(bytes.Buffer)
|
||||||
cmd.SetOut(b)
|
cmd.SetOut(b)
|
||||||
|
|||||||
@@ -3,19 +3,15 @@ package transform
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDownloadCmd(client client.Client) *cobra.Command {
|
func newDownloadCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "download",
|
Use: "download",
|
||||||
Short: "download transforms",
|
Short: "download transforms",
|
||||||
@@ -24,44 +20,21 @@ func newDownloadCmd(client client.Client) *cobra.Command {
|
|||||||
Aliases: []string{"dl"},
|
Aliases: []string{"dl"},
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
endpoint := cmd.Flags().Lookup("transforms-endpoint").Value.String()
|
|
||||||
|
|
||||||
resp, err := client.Get(cmd.Context(), endpoint)
|
transforms, err := GetTransforms()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
_ = Body.Close()
|
|
||||||
}(resp.Body)
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
return fmt.Errorf("non-200 response: %s\nbody: %s", resp.Status, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we just want to save the content to files, we don't need
|
|
||||||
// to parse individual fields. Just get the string representation.
|
|
||||||
var transforms []map[string]interface{}
|
|
||||||
|
|
||||||
err = json.Unmarshal(raw, &transforms)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
destination := cmd.Flags().Lookup("destination").Value.String()
|
destination := cmd.Flags().Lookup("destination").Value.String()
|
||||||
|
|
||||||
err = listTransforms(client, endpoint, cmd)
|
err = ListTransforms()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range transforms {
|
for _, v := range transforms {
|
||||||
filename := strings.ReplaceAll(v["name"].(string), " ", "") + ".json"
|
filename := strings.ReplaceAll(v.Name, " ", "") + ".json"
|
||||||
content, _ := json.MarshalIndent(v, "", " ")
|
content, _ := json.MarshalIndent(v, "", " ")
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func TestNewDownloadCmd(t *testing.T) {
|
|||||||
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
|
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
|
||||||
Times(1)
|
Times(1)
|
||||||
|
|
||||||
cmd := newListCmd(client)
|
cmd := newListCmd()
|
||||||
|
|
||||||
b := new(bytes.Buffer)
|
b := new(bytes.Buffer)
|
||||||
cmd.SetOut(b)
|
cmd.SetOut(b)
|
||||||
|
|||||||
@@ -2,63 +2,10 @@
|
|||||||
package transform
|
package transform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/olekukonko/tablewriter"
|
|
||||||
transmodel "github.com/sailpoint-oss/sailpoint-cli/cmd/transform/model"
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getTransforms(client client.Client, endpoint string, cmd *cobra.Command) ([]transmodel.Transform, error) {
|
func newListCmd() *cobra.Command {
|
||||||
resp, err := client.Get(cmd.Context(), endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
_ = Body.Close()
|
|
||||||
}(resp.Body)
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
return nil, fmt.Errorf("non-200 response: %s\nbody: %s", resp.Status, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var transforms []transmodel.Transform
|
|
||||||
err = json.Unmarshal(raw, &transforms)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return transforms, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func listTransforms(client client.Client, endpoint string, cmd *cobra.Command) error {
|
|
||||||
|
|
||||||
transforms, err := getTransforms(client, endpoint, cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
table := tablewriter.NewWriter(cmd.OutOrStdout())
|
|
||||||
table.SetHeader(transmodel.TransformColumns)
|
|
||||||
for _, v := range transforms {
|
|
||||||
table.Append(v.TransformToColumns())
|
|
||||||
}
|
|
||||||
table.Render()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newListCmd(client client.Client) *cobra.Command {
|
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "list transforms",
|
Short: "list transforms",
|
||||||
@@ -67,9 +14,8 @@ func newListCmd(client client.Client) *cobra.Command {
|
|||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
endpoint := cmd.Flags().Lookup("transforms-endpoint").Value.String()
|
|
||||||
|
|
||||||
err := listTransforms(client, endpoint, cmd)
|
err := ListTransforms()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestNewListCmd(t *testing.T) {
|
|||||||
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
|
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
|
||||||
Times(1)
|
Times(1)
|
||||||
|
|
||||||
cmd := newListCmd(client)
|
cmd := newListCmd()
|
||||||
|
|
||||||
b := new(bytes.Buffer)
|
b := new(bytes.Buffer)
|
||||||
cmd.SetOut(b)
|
cmd.SetOut(b)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
transmodel "github.com/sailpoint-oss/sailpoint-cli/cmd/transform/model"
|
transmodel "github.com/sailpoint-oss/sailpoint-cli/cmd/transform/model"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@@ -19,7 +20,7 @@ import (
|
|||||||
|
|
||||||
var implicitInput bool
|
var implicitInput bool
|
||||||
|
|
||||||
func newPreviewCmd(client client.Client) *cobra.Command {
|
func newPreviewCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "preview",
|
Use: "preview",
|
||||||
Short: "preview transform",
|
Short: "preview transform",
|
||||||
@@ -31,6 +32,13 @@ func newPreviewCmd(client client.Client) *cobra.Command {
|
|||||||
idProfile := cmd.Flags().Lookup("identity-profile").Value.String()
|
idProfile := cmd.Flags().Lookup("identity-profile").Value.String()
|
||||||
attribute := cmd.Flags().Lookup("attribute").Value.String()
|
attribute := cmd.Flags().Lookup("attribute").Value.String()
|
||||||
|
|
||||||
|
Config, err := config.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Client := client.NewSpClient(Config)
|
||||||
|
|
||||||
var transform map[string]interface{}
|
var transform map[string]interface{}
|
||||||
|
|
||||||
if !implicitInput {
|
if !implicitInput {
|
||||||
@@ -58,7 +66,7 @@ func newPreviewCmd(client client.Client) *cobra.Command {
|
|||||||
// original transform for the attribute, which will contain the account attribute
|
// original transform for the attribute, which will contain the account attribute
|
||||||
// name and source name that will be used in the preview body.
|
// name and source name that will be used in the preview body.
|
||||||
endpoint := cmd.Flags().Lookup("identity-profile-endpoint").Value.String()
|
endpoint := cmd.Flags().Lookup("identity-profile-endpoint").Value.String()
|
||||||
resp, err := client.Get(cmd.Context(), util.ResourceUrl(endpoint, idProfile))
|
resp, err := Client.Get(cmd.Context(), util.ResourceUrl(endpoint, idProfile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -94,7 +102,7 @@ func newPreviewCmd(client client.Client) *cobra.Command {
|
|||||||
query.Add("filters", "[{\"property\":\"links.application.id\",\"operation\":\"EQ\",\"value\":\""+profile.AuthoritativeSource.Id+"\"}]")
|
query.Add("filters", "[{\"property\":\"links.application.id\",\"operation\":\"EQ\",\"value\":\""+profile.AuthoritativeSource.Id+"\"}]")
|
||||||
uri.RawQuery = query.Encode()
|
uri.RawQuery = query.Encode()
|
||||||
|
|
||||||
resp, err = client.Get(cmd.Context(), uri.String())
|
resp, err = Client.Get(cmd.Context(), uri.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -163,7 +171,7 @@ func newPreviewCmd(client client.Client) *cobra.Command {
|
|||||||
|
|
||||||
// Call the preview endpoint to get the raw and transformed attribute values
|
// Call the preview endpoint to get the raw and transformed attribute values
|
||||||
endpoint = cmd.Flags().Lookup("preview-endpoint").Value.String()
|
endpoint = cmd.Flags().Lookup("preview-endpoint").Value.String()
|
||||||
resp, err = client.Post(cmd.Context(), util.ResourceUrl(endpoint, user[0].Id), "application/json", bytes.NewReader(previewBodyRaw))
|
resp, err = Client.Post(cmd.Context(), util.ResourceUrl(endpoint, user[0].Id), "application/json", bytes.NewReader(previewBodyRaw))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,15 @@
|
|||||||
package transform
|
package transform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
"github.com/olekukonko/tablewriter"
|
||||||
|
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
||||||
|
sailpointsdk "github.com/sailpoint-oss/golang-sdk/sdk-output/v3"
|
||||||
|
transmodel "github.com/sailpoint-oss/sailpoint-cli/cmd/transform/model"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +21,34 @@ const (
|
|||||||
userEndpoint = "/cc/api/identity/list"
|
userEndpoint = "/cc/api/identity/list"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewTransformCmd(client client.Client) *cobra.Command {
|
func GetTransforms() ([]sailpointsdk.Transform, error) {
|
||||||
|
apiClient := config.InitAPIClient()
|
||||||
|
transforms, _, err := sailpoint.PaginateWithDefaults[sailpointsdk.Transform](apiClient.V3.TransformsApi.GetTransformsList(context.TODO()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transforms, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListTransforms() error {
|
||||||
|
|
||||||
|
transforms, err := GetTransforms()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
|
table.SetHeader(transmodel.TransformColumns)
|
||||||
|
for _, v := range transforms {
|
||||||
|
table.Append([]string{*v.Id, v.Name})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransformCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "transform",
|
Use: "transform",
|
||||||
Short: "manage transforms",
|
Short: "manage transforms",
|
||||||
@@ -28,12 +61,12 @@ func NewTransformCmd(client client.Client) *cobra.Command {
|
|||||||
cmd.PersistentFlags().StringP("transforms-endpoint", "e", transformsEndpoint, "Override transforms endpoint")
|
cmd.PersistentFlags().StringP("transforms-endpoint", "e", transformsEndpoint, "Override transforms endpoint")
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newListCmd(client),
|
newListCmd(),
|
||||||
newDownloadCmd(client),
|
newDownloadCmd(),
|
||||||
newCreateCmd(client),
|
newCreateCmd(),
|
||||||
newUpdateCmd(client),
|
newUpdateCmd(),
|
||||||
newDeleteCmd(client),
|
newDeleteCmd(),
|
||||||
newPreviewCmd(client),
|
newPreviewCmd(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
@@ -2,19 +2,17 @@
|
|||||||
package transform
|
package transform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
sailpointsdk "github.com/sailpoint-oss/golang-sdk/sdk-output/v3"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newUpdateCmd(client client.Client) *cobra.Command {
|
func newUpdateCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "update",
|
Use: "update",
|
||||||
Short: "update transform",
|
Short: "update transform",
|
||||||
@@ -51,25 +49,14 @@ func newUpdateCmd(client client.Client) *cobra.Command {
|
|||||||
id := data["id"].(string)
|
id := data["id"].(string)
|
||||||
delete(data, "id") // ID can't be present in the update payload
|
delete(data, "id") // ID can't be present in the update payload
|
||||||
|
|
||||||
raw, err := json.Marshal(data)
|
transform := sailpointsdk.NewTransform(data["name"].(string), data["type"].(string), data["attributes"].(map[string]interface{}))
|
||||||
|
|
||||||
|
apiClient := config.InitAPIClient()
|
||||||
|
_, _, err := apiClient.V3.TransformsApi.UpdateTransform(context.TODO(), id).Transform(*transform).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint := cmd.Flags().Lookup("transforms-endpoint").Value.String()
|
|
||||||
resp, err := client.Put(cmd.Context(), util.ResourceUrl(endpoint, id), "application/json", bytes.NewReader(raw))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
_ = Body.Close()
|
|
||||||
}(resp.Body)
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
return fmt.Errorf("update transform failed. status: %s\nbody: %s", resp.Status, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func TestNewUpdateCmd(t *testing.T) {
|
|||||||
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil).
|
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil).
|
||||||
Times(1)
|
Times(1)
|
||||||
|
|
||||||
cmd := newUpdateCmd(client)
|
cmd := newUpdateCmd()
|
||||||
|
|
||||||
b := new(bytes.Buffer)
|
b := new(bytes.Buffer)
|
||||||
cmd.SetOut(b)
|
cmd.SetOut(b)
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/terminal"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/va"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ func newCollectCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for credential := 0; credential < len(args); credential++ {
|
for credential := 0; credential < len(args); credential++ {
|
||||||
password, _ := util.PromptPassword(fmt.Sprintf("Enter Password for %v:", args[credential]))
|
password, _ := terminal.PromptPassword(fmt.Sprintf("Enter Password for %v:", args[credential]))
|
||||||
credentials = append(credentials, password)
|
credentials = append(credentials, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ func newCollectCmd() *cobra.Command {
|
|||||||
password := credentials[host]
|
password := credentials[host]
|
||||||
outputFolder := path.Join(output, endpoint)
|
outputFolder := path.Join(output, endpoint)
|
||||||
|
|
||||||
err := util.CollectVAFiles(endpoint, password, outputFolder, files)
|
err := va.CollectVAFiles(endpoint, password, outputFolder, files)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/terminal"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/va"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ func newTroubleshootCmd() *cobra.Command {
|
|||||||
|
|
||||||
var credentials []string
|
var credentials []string
|
||||||
for credential := 0; credential < len(args); credential++ {
|
for credential := 0; credential < len(args); credential++ {
|
||||||
password, _ := util.PromptPassword(fmt.Sprintf("Enter Password for %v:", args[credential]))
|
password, _ := terminal.PromptPassword(fmt.Sprintf("Enter Password for %v:", args[credential]))
|
||||||
credentials = append(credentials, password)
|
credentials = append(credentials, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ func newTroubleshootCmd() *cobra.Command {
|
|||||||
|
|
||||||
password := credentials[host]
|
password := credentials[host]
|
||||||
|
|
||||||
orgErr := util.RunVACmdLive(endpoint, password, "/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/luke-hagar-sp/VA-Scripts/main/stunt.sh)\"")
|
orgErr := va.RunVACmdLive(endpoint, password, "/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/luke-hagar-sp/VA-Scripts/main/stunt.sh)\"")
|
||||||
if orgErr != nil {
|
if orgErr != nil {
|
||||||
return orgErr
|
return orgErr
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ func newTroubleshootCmd() *cobra.Command {
|
|||||||
color.Green("Troubleshooting Complete")
|
color.Green("Troubleshooting Complete")
|
||||||
color.Blue("Collecting stuntlog")
|
color.Blue("Collecting stuntlog")
|
||||||
|
|
||||||
err := util.CollectVAFiles(endpoint, password, outputDir, []string{"/home/sailpoint/stuntlog.txt"})
|
err := va.CollectVAFiles(endpoint, password, outputDir, []string{"/home/sailpoint/stuntlog.txt"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/terminal"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/va"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,20 +19,20 @@ func newUpdateCmd() *cobra.Command {
|
|||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
var credentials []string
|
var credentials []string
|
||||||
for credential := 0; credential < len(args); credential++ {
|
for credential := 0; credential < len(args); credential++ {
|
||||||
password, _ := util.PromptPassword(fmt.Sprintf("Enter Password for %v:", args[credential]))
|
password, _ := terminal.PromptPassword(fmt.Sprintf("Enter Password for %v:", args[credential]))
|
||||||
credentials = append(credentials, password)
|
credentials = append(credentials, password)
|
||||||
}
|
}
|
||||||
for i := 0; i < len(args); i++ {
|
for i := 0; i < len(args); i++ {
|
||||||
endpoint := args[i]
|
endpoint := args[i]
|
||||||
fmt.Printf("Starting update for %v\n", endpoint)
|
fmt.Printf("Starting update for %v\n", endpoint)
|
||||||
password := credentials[i]
|
password := credentials[i]
|
||||||
_, updateErr := util.RunVACmd(endpoint, password, "sudo update_engine_client -check_for_update")
|
_, updateErr := va.RunVACmd(endpoint, password, "sudo update_engine_client -check_for_update")
|
||||||
if updateErr != nil {
|
if updateErr != nil {
|
||||||
return updateErr
|
return updateErr
|
||||||
} else {
|
} else {
|
||||||
color.Green("Initiating update check and install (%v)", endpoint)
|
color.Green("Initiating update check and install (%v)", endpoint)
|
||||||
}
|
}
|
||||||
reboot, rebootErr := util.RunVACmd(endpoint, password, "sudo reboot")
|
reboot, rebootErr := va.RunVACmd(endpoint, password, "sudo reboot")
|
||||||
if rebootErr != nil {
|
if rebootErr != nil {
|
||||||
color.Green("Rebooting Virtual Appliance (%v)", endpoint)
|
color.Green("Rebooting Virtual Appliance (%v)", endpoint)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -7,6 +7,7 @@ require (
|
|||||||
github.com/charmbracelet/bubbletea v0.23.1
|
github.com/charmbracelet/bubbletea v0.23.1
|
||||||
github.com/charmbracelet/lipgloss v0.6.0
|
github.com/charmbracelet/lipgloss v0.6.0
|
||||||
github.com/fatih/color v1.13.0
|
github.com/fatih/color v1.13.0
|
||||||
|
github.com/gocarina/gocsv v0.0.0-20230123225133-763e25b40669
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/kr/pretty v0.3.1
|
github.com/kr/pretty v0.3.1
|
||||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||||
@@ -20,6 +21,7 @@ require (
|
|||||||
github.com/spf13/viper v1.15.0
|
github.com/spf13/viper v1.15.0
|
||||||
github.com/vbauerster/mpb/v8 v8.1.4
|
github.com/vbauerster/mpb/v8 v8.1.4
|
||||||
golang.org/x/crypto v0.5.0
|
golang.org/x/crypto v0.5.0
|
||||||
|
golang.org/x/exp v0.0.0-20230206171751-46f607a40771
|
||||||
golang.org/x/oauth2 v0.4.0
|
golang.org/x/oauth2 v0.4.0
|
||||||
golang.org/x/term v0.4.0
|
golang.org/x/term v0.4.0
|
||||||
gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61
|
gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -85,6 +85,8 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS
|
|||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/gocarina/gocsv v0.0.0-20230123225133-763e25b40669 h1:MvZzCA/mduVWoBSVKJeMdv+AqXQmZZ8i6p8889ejt/Y=
|
||||||
|
github.com/gocarina/gocsv v0.0.0-20230123225133-763e25b40669/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@@ -302,6 +304,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
|||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
|
golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg=
|
||||||
|
golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import (
|
|||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
clierrors "github.com/sailpoint-oss/sailpoint-cli/internal/errors"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/types"
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,12 +23,12 @@ type Client interface {
|
|||||||
|
|
||||||
// SpClient provides access to SP APIs.
|
// SpClient provides access to SP APIs.
|
||||||
type SpClient struct {
|
type SpClient struct {
|
||||||
cfg types.CLIConfig
|
cfg config.CLIConfig
|
||||||
client *http.Client
|
client *http.Client
|
||||||
accessToken string
|
accessToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSpClient(cfg types.CLIConfig) Client {
|
func NewSpClient(cfg config.CLIConfig) Client {
|
||||||
return &SpClient{
|
return &SpClient{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
client: &http.Client{},
|
client: &http.Client{},
|
||||||
@@ -41,10 +40,7 @@ func (c *SpClient) Get(ctx context.Context, url string) (*http.Response, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
baseUrl, err := c.cfg.GetBaseUrl()
|
baseUrl := config.GetBaseUrl()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseUrl+url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseUrl+url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,10 +70,7 @@ func (c *SpClient) Delete(ctx context.Context, url string, params map[string]str
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
baseUrl, err := c.cfg.GetBaseUrl()
|
baseUrl := config.GetBaseUrl()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, baseUrl+url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, baseUrl+url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -115,10 +108,7 @@ func (c *SpClient) Post(ctx context.Context, url string, contentType string, bod
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
baseUrl, err := c.cfg.GetBaseUrl()
|
baseUrl := config.GetBaseUrl()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseUrl+url, body)
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseUrl+url, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -149,10 +139,7 @@ func (c *SpClient) Put(ctx context.Context, url string, contentType string, body
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
baseUrl, err := c.cfg.GetBaseUrl()
|
baseUrl := config.GetBaseUrl()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, baseUrl+url, body)
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, baseUrl+url, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -180,7 +167,7 @@ func (c *SpClient) Put(ctx context.Context, url string, contentType string, body
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *SpClient) ensureAccessToken(ctx context.Context) error {
|
func (c *SpClient) ensureAccessToken(ctx context.Context) error {
|
||||||
err := c.cfg.Validate()
|
err := config.Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -190,30 +177,32 @@ func (c *SpClient) ensureAccessToken(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var cachedTokenExpiry time.Time
|
var cachedTokenExpiry time.Time
|
||||||
switch c.cfg.GetAuthType() {
|
switch config.GetAuthType() {
|
||||||
case "pat":
|
case "pat":
|
||||||
cachedTokenExpiry = viper.GetTime("pat.token.expiry")
|
cachedTokenExpiry = viper.GetTime("pat.token.expiry")
|
||||||
if cachedTokenExpiry.After(time.Now()) {
|
if cachedTokenExpiry.After(time.Now()) {
|
||||||
c.accessToken = viper.GetString("pat.token.accesstoken")
|
c.accessToken = viper.GetString("pat.token.accesstoken")
|
||||||
} else {
|
} else {
|
||||||
return clierrors.ErrAccessTokenExpired
|
err := config.PATLogin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case "oauth":
|
case "oauth":
|
||||||
cachedTokenExpiry = viper.GetTime("oauth.token.expiry")
|
cachedTokenExpiry = viper.GetTime("oauth.token.expiry")
|
||||||
if cachedTokenExpiry.After(time.Now()) {
|
if cachedTokenExpiry.After(time.Now()) {
|
||||||
c.accessToken = viper.GetString("oauth.token.accesstoken")
|
c.accessToken = viper.GetString("oauth.token.accesstoken")
|
||||||
} else {
|
} else {
|
||||||
return clierrors.ErrAccessTokenExpired
|
err := config.OAuthLogin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return errors.New("invalid authtype configured")
|
return errors.New("invalid authtype configured")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.accessToken != "" {
|
|
||||||
return fmt.Errorf("no token present")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package errors
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
var ErrAccessTokenExpired = fmt.Errorf("accesstoken is expired")
|
|
||||||
64
internal/output/output.go
Normal file
64
internal/output/output.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/gocarina/gocsv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SaveJSONFile[T any](formattedResponse T, fileName string, folderPath string) error {
|
||||||
|
|
||||||
|
savePath := path.Join(folderPath, fileName)
|
||||||
|
|
||||||
|
dataToSave, err := json.MarshalIndent(formattedResponse, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the output dir exists first
|
||||||
|
err = os.MkdirAll(folderPath, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(savePath, os.O_CREATE|os.O_RDWR, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileWriter := bufio.NewWriter(file)
|
||||||
|
|
||||||
|
_, err = fileWriter.Write(dataToSave)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveCSVFile[T any](formattedResponse T, fileName string, folderPath string) error {
|
||||||
|
savePath := path.Join(folderPath, fileName)
|
||||||
|
|
||||||
|
// Make sure the output dir exists first
|
||||||
|
err := os.MkdirAll(folderPath, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(savePath, os.O_CREATE|os.O_RDWR, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
err = gocsv.MarshalFile(formattedResponse, file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
196
internal/search/search.go
Normal file
196
internal/search/search.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
||||||
|
sailpointsdk "github.com/sailpoint-oss/golang-sdk/sdk-output/v3"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/output"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseIndicie(indicie string) (sailpointsdk.Index, error) {
|
||||||
|
switch indicie {
|
||||||
|
case "accessprofiles":
|
||||||
|
return sailpointsdk.INDEX_ACCESSPROFILES, nil
|
||||||
|
case "accountactivities":
|
||||||
|
return sailpointsdk.INDEX_ACCOUNTACTIVITIES, nil
|
||||||
|
case "entitlements":
|
||||||
|
return sailpointsdk.INDEX_ENTITLEMENTS, nil
|
||||||
|
case "events":
|
||||||
|
return sailpointsdk.INDEX_EVENTS, nil
|
||||||
|
case "identities":
|
||||||
|
return sailpointsdk.INDEX_IDENTITIES, nil
|
||||||
|
case "roles":
|
||||||
|
return sailpointsdk.INDEX_ROLES, nil
|
||||||
|
}
|
||||||
|
return "*", fmt.Errorf("indicie provided is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildSearch(searchQuery string, sort []string, indicies []string) (sailpointsdk.Search1, error) {
|
||||||
|
|
||||||
|
search := sailpointsdk.NewSearch1()
|
||||||
|
search.Query = sailpointsdk.NewQuery()
|
||||||
|
search.Query.Query = &searchQuery
|
||||||
|
search.Sort = sort
|
||||||
|
search.Indices = []sailpointsdk.Index{}
|
||||||
|
|
||||||
|
for i := 0; i < len(indicies); i++ {
|
||||||
|
tempIndicie, err := ParseIndicie(indicies[i])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return *search, err
|
||||||
|
}
|
||||||
|
|
||||||
|
search.Indices = append(search.Indices, tempIndicie)
|
||||||
|
}
|
||||||
|
|
||||||
|
return *search, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func PerformSearch(apiClient sailpoint.APIClient, search sailpointsdk.Search1) (SearchResults, error) {
|
||||||
|
var SearchResults SearchResults
|
||||||
|
|
||||||
|
ctx := context.TODO()
|
||||||
|
resp, r, err := sailpoint.PaginateWithDefaults[map[string]interface{}](apiClient.V3.SearchApi.SearchPost(ctx).Search1(search))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
color.Green("Search complete, saving results")
|
||||||
|
|
||||||
|
for i := 0; i < len(resp); i++ {
|
||||||
|
entry := resp[i]
|
||||||
|
switch entry["_type"] {
|
||||||
|
case "accountactivity":
|
||||||
|
var AccountActivity AccountActivity
|
||||||
|
err := mapstructure.Decode(entry, &AccountActivity)
|
||||||
|
if err != nil {
|
||||||
|
return SearchResults, err
|
||||||
|
}
|
||||||
|
SearchResults.AccountActivities = append(SearchResults.AccountActivities, AccountActivity)
|
||||||
|
|
||||||
|
case "accessprofile":
|
||||||
|
var AccessProfile AccessProfile
|
||||||
|
err := mapstructure.Decode(entry, &AccessProfile)
|
||||||
|
if err != nil {
|
||||||
|
return SearchResults, err
|
||||||
|
}
|
||||||
|
SearchResults.AccessProfiles = append(SearchResults.AccessProfiles, AccessProfile)
|
||||||
|
|
||||||
|
case "entitlement":
|
||||||
|
var Entitlement Entitlement
|
||||||
|
err := mapstructure.Decode(entry, &Entitlement)
|
||||||
|
if err != nil {
|
||||||
|
return SearchResults, err
|
||||||
|
}
|
||||||
|
SearchResults.Entitlements = append(SearchResults.Entitlements, Entitlement)
|
||||||
|
|
||||||
|
case "event":
|
||||||
|
var Event Event
|
||||||
|
err := mapstructure.Decode(entry, &Event)
|
||||||
|
if err != nil {
|
||||||
|
return SearchResults, err
|
||||||
|
}
|
||||||
|
SearchResults.Events = append(SearchResults.Events, Event)
|
||||||
|
|
||||||
|
case "identity":
|
||||||
|
var Identity Identity
|
||||||
|
err := mapstructure.Decode(entry, &Identity)
|
||||||
|
if err != nil {
|
||||||
|
return SearchResults, err
|
||||||
|
}
|
||||||
|
SearchResults.Identities = append(SearchResults.Identities, Identity)
|
||||||
|
|
||||||
|
case "role":
|
||||||
|
var Role Role
|
||||||
|
err := mapstructure.Decode(entry, &Role)
|
||||||
|
if err != nil {
|
||||||
|
return SearchResults, err
|
||||||
|
}
|
||||||
|
SearchResults.Roles = append(SearchResults.Roles, Role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SearchResults, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IterateIndicies(SearchResults SearchResults, searchQuery string, folderPath string, outputTypes []string) error {
|
||||||
|
var err error
|
||||||
|
if len(SearchResults.AccountActivities) > 0 {
|
||||||
|
fileName := fmt.Sprintf("query=%s&indicie=%s", searchQuery, "AccountActivities")
|
||||||
|
err = SaveResults(SearchResults.AccountActivities, fileName, folderPath, outputTypes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(SearchResults.AccessProfiles) > 0 {
|
||||||
|
fileName := fmt.Sprintf("query=%s&indicie=%s", searchQuery, "AccessProfiles")
|
||||||
|
err = SaveResults(SearchResults.AccessProfiles, fileName, folderPath, outputTypes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(SearchResults.Entitlements) > 0 {
|
||||||
|
fileName := fmt.Sprintf("query=%s&indicie=%s", searchQuery, "Entitlements")
|
||||||
|
err = SaveResults(SearchResults.Entitlements, fileName, folderPath, outputTypes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(SearchResults.Events) > 0 {
|
||||||
|
fileName := fmt.Sprintf("query=%s&indicie=%s", searchQuery, "Events")
|
||||||
|
err = SaveResults(SearchResults.Events, fileName, folderPath, outputTypes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(SearchResults.Identities) > 0 {
|
||||||
|
fileName := fmt.Sprintf("query=%s&indicie=%s", searchQuery, "Identities")
|
||||||
|
err = SaveResults(SearchResults.Identities, fileName, folderPath, outputTypes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(SearchResults.Roles) > 0 {
|
||||||
|
fileName := fmt.Sprintf("query=%s&indicie=%s", searchQuery, "Roles")
|
||||||
|
err = SaveResults(SearchResults.Roles, fileName, folderPath, outputTypes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveResults[T any](formattedResponse []T, fileName string, filePath string, outputTypes []string) error {
|
||||||
|
for i := 0; i < len(outputTypes); i++ {
|
||||||
|
outputType := outputTypes[i]
|
||||||
|
switch outputType {
|
||||||
|
case "json":
|
||||||
|
fileName = fileName + ".json"
|
||||||
|
savePath := path.Join(filePath, fileName)
|
||||||
|
err := output.SaveJSONFile(formattedResponse, fileName, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
color.Green("Saving file: %s", savePath)
|
||||||
|
case "csv":
|
||||||
|
fileName = fileName + ".csv"
|
||||||
|
savePath := path.Join(filePath, fileName)
|
||||||
|
err := output.SaveCSVFile(formattedResponse, fileName, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
color.Green("Saving file: %s", savePath)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid output type provided %s", outputType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
292
internal/search/types.go
Normal file
292
internal/search/types.go
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
type SearchResults struct {
|
||||||
|
AccountActivities []AccountActivity
|
||||||
|
AccessProfiles []AccessProfile
|
||||||
|
Entitlements []Entitlement
|
||||||
|
Events []Event
|
||||||
|
Identities []Identity
|
||||||
|
Roles []Role
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessProfile struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"_type"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
Modified string `json:"modified"`
|
||||||
|
Synced string `json:"synced"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Requestable bool `json:"requestable"`
|
||||||
|
RequestCommentsRequired bool `json:"requestCommentsRequired"`
|
||||||
|
Owner struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
} `json:"owner"`
|
||||||
|
Source struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"source"`
|
||||||
|
Entitlements []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Attribute string `json:"attribute"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
} `json:"entitlements"`
|
||||||
|
EntitlementCount int `json:"entitlementCount"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountActivity struct {
|
||||||
|
Requester struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
} `json:"requester,omitempty"`
|
||||||
|
Sources string `json:"sources,omitempty"`
|
||||||
|
Created string `json:"created,omitempty"`
|
||||||
|
Warnings []string `json:"warnings,omitempty"`
|
||||||
|
AccountRequests []struct {
|
||||||
|
Result struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
} `json:"result,omitempty"`
|
||||||
|
AccountID string `json:"accountId,omitempty"`
|
||||||
|
Op string `json:"op,omitempty"`
|
||||||
|
AttributeRequests []struct {
|
||||||
|
Op string `json:"op,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
} `json:"attributeRequests,omitempty"`
|
||||||
|
ProvisioningTarget struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
} `json:"provisioningTarget,omitempty"`
|
||||||
|
Source struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
} `json:"source,omitempty"`
|
||||||
|
} `json:"accountRequests,omitempty"`
|
||||||
|
Stage string `json:"stage,omitempty"`
|
||||||
|
OriginalRequests []struct {
|
||||||
|
Result struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
} `json:"result,omitempty"`
|
||||||
|
AccountID string `json:"accountId,omitempty"`
|
||||||
|
Op string `json:"op,omitempty"`
|
||||||
|
Source struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
} `json:"source,omitempty"`
|
||||||
|
AttributeRequests []struct {
|
||||||
|
Op string `json:"op,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
} `json:"attributeRequests,omitempty"`
|
||||||
|
} `json:"originalRequests,omitempty"`
|
||||||
|
ExpansionItems []interface{} `json:"expansionItems,omitempty"`
|
||||||
|
Approvals []struct {
|
||||||
|
AttributeRequest struct {
|
||||||
|
Op string `json:"op,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
} `json:"attributeRequest,omitempty"`
|
||||||
|
Source struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
} `json:"source,omitempty"`
|
||||||
|
} `json:"approvals,omitempty"`
|
||||||
|
Recipient struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
} `json:"recipient,omitempty"`
|
||||||
|
Action string `json:"action,omitempty"`
|
||||||
|
Modified string `json:"modified,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
TrackingNumber string `json:"trackingNumber,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Pod string `json:"pod,omitempty"`
|
||||||
|
Org string `json:"org,omitempty"`
|
||||||
|
Synced string `json:"synced,omitempty"`
|
||||||
|
Type string `json:"_type,omitempty"`
|
||||||
|
Type0 string `json:"type,omitempty"`
|
||||||
|
Version string `json:"_version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Entitlement struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"_type"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Attribute string `json:"attribute"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Modified string `json:"modified"`
|
||||||
|
Synced string `json:"synced"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Source struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"source"`
|
||||||
|
Privileged bool `json:"privileged"`
|
||||||
|
IdentityCount int `json:"identityCount"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"_type"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
Synced string `json:"synced"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Type0 string `json:"type"`
|
||||||
|
Actor struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"actor"`
|
||||||
|
Target struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"target"`
|
||||||
|
Stack string `json:"stack"`
|
||||||
|
TrackingNumber string `json:"trackingNumber"`
|
||||||
|
IPAddress string `json:"ipAddress"`
|
||||||
|
Details string `json:"details"`
|
||||||
|
Attributes struct {
|
||||||
|
SourceName string `json:"sourceName"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
Objects []string `json:"objects"`
|
||||||
|
Operation string `json:"operation"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
TechnicalName string `json:"technicalName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Identity struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"_type"`
|
||||||
|
FirstName string `json:"firstName"`
|
||||||
|
LastName string `json:"lastName"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
Modified string `json:"modified"`
|
||||||
|
Synced string `json:"synced"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Inactive bool `json:"inactive"`
|
||||||
|
Protected bool `json:"protected"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
EmployeeNumber string `json:"employeeNumber"`
|
||||||
|
Manager interface{} `json:"manager"`
|
||||||
|
IsManager bool `json:"isManager"`
|
||||||
|
IdentityProfile struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"identityProfile"`
|
||||||
|
Source struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"source"`
|
||||||
|
Attributes struct {
|
||||||
|
UID string `json:"uid"`
|
||||||
|
Firstname string `json:"firstname"`
|
||||||
|
CloudAuthoritativeSource string `json:"cloudAuthoritativeSource"`
|
||||||
|
CloudStatus string `json:"cloudStatus"`
|
||||||
|
IplanetAmUserAliasList interface{} `json:"iplanet-am-user-alias-list"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
InternalCloudStatus string `json:"internalCloudStatus"`
|
||||||
|
WorkPhone string `json:"workPhone"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Lastname string `json:"lastname"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
ProcessingState interface{} `json:"processingState"`
|
||||||
|
ProcessingDetails interface{} `json:"processingDetails"`
|
||||||
|
Accounts []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
AccountID string `json:"accountId"`
|
||||||
|
Source struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
} `json:"source"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
Locked bool `json:"locked"`
|
||||||
|
Privileged bool `json:"privileged"`
|
||||||
|
ManuallyCorrelated bool `json:"manuallyCorrelated"`
|
||||||
|
PasswordLastSet string `json:"passwordLastSet"`
|
||||||
|
EntitlementAttributes struct {
|
||||||
|
MemberOf []string `json:"memberOf"`
|
||||||
|
} `json:"entitlementAttributes"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
} `json:"accounts"`
|
||||||
|
AccountCount int `json:"accountCount"`
|
||||||
|
Apps []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Source struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"source"`
|
||||||
|
Account struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
AccountID string `json:"accountId"`
|
||||||
|
} `json:"account"`
|
||||||
|
} `json:"apps"`
|
||||||
|
AppCount int `json:"appCount"`
|
||||||
|
Access []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Source struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"source,omitempty"`
|
||||||
|
Owner struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
} `json:"owner,omitempty"`
|
||||||
|
Privileged bool `json:"privileged,omitempty"`
|
||||||
|
Attribute string `json:"attribute,omitempty"`
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
Standalone bool `json:"standalone,omitempty"`
|
||||||
|
Disabled bool `json:"disabled,omitempty"`
|
||||||
|
} `json:"access"`
|
||||||
|
AccessCount int `json:"accessCount"`
|
||||||
|
AccessProfileCount int `json:"accessProfileCount"`
|
||||||
|
EntitlementCount int `json:"entitlementCount"`
|
||||||
|
RoleCount int `json:"roleCount"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Role struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"_type"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
Modified interface{} `json:"modified"`
|
||||||
|
Synced string `json:"synced"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Requestable bool `json:"requestable"`
|
||||||
|
RequestCommentsRequired bool `json:"requestCommentsRequired"`
|
||||||
|
Owner struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
} `json:"owner"`
|
||||||
|
AccessProfiles []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"accessProfiles"`
|
||||||
|
AccessProfileCount int `json:"accessProfileCount"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
51
internal/spconfig/spconfig.go
Normal file
51
internal/spconfig/spconfig.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package spconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
sailpointbetasdk "github.com/sailpoint-oss/golang-sdk/sdk-output/beta"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
|
"github.com/sailpoint-oss/sailpoint-cli/internal/output"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrintJob(job sailpointbetasdk.SpConfigJob) {
|
||||||
|
fmt.Printf("Job Type: %s\nJob ID: %s\nStatus: %s\nExpired: %s\nCreated: %s\nModified: %s\nCompleted: %s\n", job.Type, job.JobId, job.Status, job.GetExpiration(), job.GetCreated(), job.GetModified(), job.GetCompleted())
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadExport(jobId string, fileName string, folderPath string) error {
|
||||||
|
apiClient := config.InitAPIClient()
|
||||||
|
|
||||||
|
for {
|
||||||
|
response, _, err := apiClient.Beta.SPConfigApi.SpConfigExportJobStatus(context.TODO(), jobId).Execute()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if response.Status == "NOT_STARTED" || response.Status == "IN_PROGRESS" {
|
||||||
|
color.Yellow("Status: %s. checking again in 5 seconds", response.Status)
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
} else {
|
||||||
|
switch response.Status {
|
||||||
|
case "COMPLETE":
|
||||||
|
color.Green("Downloading Export Data")
|
||||||
|
export, _, err := apiClient.Beta.SPConfigApi.SpConfigExportDownload(context.TODO(), jobId).Execute()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = output.SaveJSONFile(export, fileName, folderPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "CANCELLED":
|
||||||
|
return fmt.Errorf("export task cancelled")
|
||||||
|
case "FAILED":
|
||||||
|
return fmt.Errorf("export task failed")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var docStyle = lipgloss.NewStyle().Margin(1, 2)
|
var docStyle = lipgloss.NewStyle().Margin(1, 2)
|
||||||
@@ -16,6 +15,11 @@ type ListItem struct {
|
|||||||
description string
|
description string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Choice struct {
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
var choice ListItem
|
var choice ListItem
|
||||||
|
|
||||||
func (i ListItem) Title() string { return i.title }
|
func (i ListItem) Title() string { return i.title }
|
||||||
@@ -69,7 +73,7 @@ func (m model) Retrieve() ListItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func PromptList(choices []types.Choice, Title string) (types.Choice, error) {
|
func PromptList(choices []Choice, Title string) (Choice, error) {
|
||||||
|
|
||||||
items := []list.Item{}
|
items := []list.Item{}
|
||||||
for i := 0; i < len(choices); i++ {
|
for i := 0; i < len(choices); i++ {
|
||||||
@@ -84,10 +88,10 @@ func PromptList(choices []types.Choice, Title string) (types.Choice, error) {
|
|||||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||||
|
|
||||||
if _, err := p.Run(); err != nil {
|
if _, err := p.Run(); err != nil {
|
||||||
return types.Choice{}, fmt.Errorf("error running program: %s", err)
|
return Choice{}, fmt.Errorf("error running program: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
choice := m.Retrieve()
|
choice := m.Retrieve()
|
||||||
|
|
||||||
return types.Choice{Title: choice.title, Description: choice.description}, nil
|
return Choice{Title: choice.title, Description: choice.description}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,335 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
|
||||||
clierrors "github.com/sailpoint-oss/sailpoint-cli/internal/errors"
|
|
||||||
"github.com/skratchdot/open-golang/open"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Bundle struct {
|
|
||||||
Config CLIConfig
|
|
||||||
Client any
|
|
||||||
APIClient *sailpoint.APIClient
|
|
||||||
}
|
|
||||||
|
|
||||||
type CLIConfig struct {
|
|
||||||
CustomExportTemplatesPath string `mapstructure:"customExportTemplatesPath"`
|
|
||||||
CustomSearchTemplatesPath string `mapstructure:"customSearchTemplatesPath"`
|
|
||||||
Debug bool `mapstructure:"debug"`
|
|
||||||
AuthType string `mapstructure:"authtype"`
|
|
||||||
ActiveEnvironment string `mapstructure:"activeEnv"`
|
|
||||||
Environments map[string]Environment
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config CLIConfig) GetAuthType() string {
|
|
||||||
return strings.ToLower(config.AuthType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config CLIConfig) GetBaseUrl() (string, error) {
|
|
||||||
switch config.GetAuthType() {
|
|
||||||
case "pat":
|
|
||||||
return config.Environments[config.ActiveEnvironment].Pat.BaseUrl, nil
|
|
||||||
case "oauth":
|
|
||||||
return config.Environments[config.ActiveEnvironment].OAuth.BaseUrl, nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("configured authtype ('%s') is invalid or missing", config.AuthType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
callbackErr error
|
|
||||||
conf *oauth2.Config
|
|
||||||
ctx context.Context
|
|
||||||
server *http.Server
|
|
||||||
)
|
|
||||||
|
|
||||||
func (config CLIConfig) CallbackHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
queryParts, _ := url.ParseQuery(r.URL.RawQuery)
|
|
||||||
|
|
||||||
// Use the authorization code that is pushed to the redirect URL
|
|
||||||
code := queryParts["code"][0]
|
|
||||||
|
|
||||||
// Exchange will do the handshake to retrieve the initial access token.
|
|
||||||
tok, err := conf.Exchange(ctx, code)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The HTTP Client returned by conf.Client will refresh the token as necessary.
|
|
||||||
client := conf.Client(ctx, tok)
|
|
||||||
|
|
||||||
baseUrl, err := config.GetBaseUrl()
|
|
||||||
if err != nil {
|
|
||||||
callbackErr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Get(baseUrl + "/beta/tenant-data/hosting-data")
|
|
||||||
if err != nil {
|
|
||||||
callbackErr = err
|
|
||||||
} else {
|
|
||||||
color.Green("Authentication successful")
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
_ = Body.Close()
|
|
||||||
}(resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
viper.Set(fmt.Sprintf("environments.%s.oauth.token", config.ActiveEnvironment), Token{AccessToken: tok.AccessToken, Expiry: tok.Expiry})
|
|
||||||
|
|
||||||
config.SaveConfig()
|
|
||||||
|
|
||||||
// show succes page
|
|
||||||
msg := "<p><strong>SailPoint CLI, OAuth Login Success!</strong></p>"
|
|
||||||
msg = msg + "<p>You are authenticated and can now return to the CLI.</p>"
|
|
||||||
fmt.Fprint(w, msg)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
|
|
||||||
defer cancel()
|
|
||||||
if err := server.Shutdown(ctx); err != nil {
|
|
||||||
callbackErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config CLIConfig) OAuthLogin() error {
|
|
||||||
ctx = context.Background()
|
|
||||||
|
|
||||||
conf = &oauth2.Config{
|
|
||||||
ClientID: config.Environments[config.ActiveEnvironment].OAuth.ClientID,
|
|
||||||
ClientSecret: config.Environments[config.ActiveEnvironment].OAuth.ClientSecret,
|
|
||||||
Endpoint: oauth2.Endpoint{
|
|
||||||
AuthURL: config.Environments[config.ActiveEnvironment].OAuth.AuthUrl,
|
|
||||||
TokenURL: config.Environments[config.ActiveEnvironment].OAuth.TokenUrl,
|
|
||||||
},
|
|
||||||
RedirectURL: "http://localhost:" + fmt.Sprint(config.Environments[config.ActiveEnvironment].OAuth.Redirect.Port) + config.Environments[config.ActiveEnvironment].OAuth.Redirect.Path,
|
|
||||||
}
|
|
||||||
|
|
||||||
// add transport for self-signed certificate to context
|
|
||||||
tr := &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
}
|
|
||||||
sslClient := &http.Client{Transport: tr}
|
|
||||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, sslClient)
|
|
||||||
|
|
||||||
// Redirect user to login page
|
|
||||||
url := conf.AuthCodeURL("")
|
|
||||||
|
|
||||||
color.Green("Opening browser for authentication")
|
|
||||||
|
|
||||||
open.Run(url)
|
|
||||||
|
|
||||||
http.HandleFunc(config.Environments[config.ActiveEnvironment].OAuth.Redirect.Path, config.CallbackHandler)
|
|
||||||
server = &http.Server{Addr: fmt.Sprintf(":%v", config.Environments[config.ActiveEnvironment].OAuth.Redirect.Port), Handler: nil}
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
server.ListenAndServe()
|
|
||||||
}()
|
|
||||||
wg.Wait()
|
|
||||||
if callbackErr != nil {
|
|
||||||
return callbackErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config CLIConfig) PATLogin() error {
|
|
||||||
|
|
||||||
uri, err := url.Parse(config.Environments[config.ActiveEnvironment].Pat.TokenUrl)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
query := &url.Values{}
|
|
||||||
query.Add("grant_type", "client_credentials")
|
|
||||||
uri.RawQuery = query.Encode()
|
|
||||||
|
|
||||||
data := &url.Values{}
|
|
||||||
data.Add("client_id", config.Environments[config.ActiveEnvironment].Pat.ClientID)
|
|
||||||
data.Add("client_secret", config.Environments[config.ActiveEnvironment].Pat.ClientSecret)
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), strings.NewReader(data.Encode()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
|
|
||||||
client := http.Client{}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
_ = Body.Close()
|
|
||||||
}(resp.Body)
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("failed to retrieve access token. status %s", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var tResponse TokenResponse
|
|
||||||
|
|
||||||
err = json.Unmarshal(raw, &tResponse)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
viper.Set(fmt.Sprintf("environments.%s.pat.token", config.ActiveEnvironment), Token{AccessToken: tResponse.AccessToken, Expiry: now.Add(time.Second * time.Duration(tResponse.ExpiresIn))})
|
|
||||||
|
|
||||||
config.SaveConfig()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config CLIConfig) EnsureAccessToken() error {
|
|
||||||
err := config.Validate()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
authType := config.GetAuthType()
|
|
||||||
|
|
||||||
switch authType {
|
|
||||||
case "pat":
|
|
||||||
if config.Environments[config.ActiveEnvironment].Pat.Token.Expiry.After(time.Now()) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
case "oauth":
|
|
||||||
if config.Environments[config.ActiveEnvironment].OAuth.Token.Expiry.After(time.Now()) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return errors.New("invalid authtype configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch authType {
|
|
||||||
case "pat":
|
|
||||||
err = config.PATLogin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
case "oauth":
|
|
||||||
err = config.OAuthLogin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return errors.New("invalid authtype configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
config.SaveConfig()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config CLIConfig) SaveConfig() error {
|
|
||||||
err := viper.WriteConfig()
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
|
||||||
err = viper.SafeWriteConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config CLIConfig) GetAuthToken() (string, error) {
|
|
||||||
authType := config.GetAuthType()
|
|
||||||
switch authType {
|
|
||||||
case "pat":
|
|
||||||
expiry := viper.GetTime(fmt.Sprintf("environments.%s.pat.token.expiry", config.ActiveEnvironment))
|
|
||||||
fmt.Println(expiry)
|
|
||||||
if expiry.After(time.Now()) {
|
|
||||||
return viper.GetString(fmt.Sprintf("environments.%s.pat.token.accesstoken", config.ActiveEnvironment)), nil
|
|
||||||
} else {
|
|
||||||
return "", clierrors.ErrAccessTokenExpired
|
|
||||||
}
|
|
||||||
case "oauth":
|
|
||||||
expiry := viper.GetTime(fmt.Sprintf("environments.%s.oauth.token.expiry", config.ActiveEnvironment))
|
|
||||||
fmt.Println(expiry)
|
|
||||||
if expiry.After(time.Now()) {
|
|
||||||
return viper.GetString(fmt.Sprintf("environments.%s.oauth.token.accesstoken", config.ActiveEnvironment)), nil
|
|
||||||
} else {
|
|
||||||
return "", clierrors.ErrAccessTokenExpired
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("invalid authtype '%s' configured", config.AuthType)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config CLIConfig) GetActiveEnvironment(env string) Environment {
|
|
||||||
return config.Environments[config.ActiveEnvironment]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config CLIConfig) GetEnvironment(env string) Environment {
|
|
||||||
return config.Environments[env]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config CLIConfig) Validate() error {
|
|
||||||
switch config.GetAuthType() {
|
|
||||||
case "pat":
|
|
||||||
if config.Environments[config.ActiveEnvironment].Pat.TokenUrl == "" {
|
|
||||||
return fmt.Errorf("missing PAT TokenURL configuration value")
|
|
||||||
}
|
|
||||||
if config.Environments[config.ActiveEnvironment].Pat.ClientID == "" {
|
|
||||||
return fmt.Errorf("missing PAT ClientID configuration value")
|
|
||||||
}
|
|
||||||
if config.Environments[config.ActiveEnvironment].Pat.ClientSecret == "" {
|
|
||||||
return fmt.Errorf("missing PAT ClientSecret configuration value")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case "oauth":
|
|
||||||
if config.Environments[config.ActiveEnvironment].OAuth.AuthUrl == "" {
|
|
||||||
return fmt.Errorf("missing OAuth URL configuration value")
|
|
||||||
}
|
|
||||||
if config.Environments[config.ActiveEnvironment].OAuth.ClientID == "" {
|
|
||||||
return fmt.Errorf("missing OAuth ClientID configuration value")
|
|
||||||
}
|
|
||||||
if config.Environments[config.ActiveEnvironment].OAuth.ClientSecret == "" && config.Debug {
|
|
||||||
color.Yellow("missing OAuth ClientSecret configuration value")
|
|
||||||
}
|
|
||||||
if config.Environments[config.ActiveEnvironment].OAuth.Redirect.Path == "" {
|
|
||||||
return fmt.Errorf("missing OAuth Redirect Path configuration value")
|
|
||||||
}
|
|
||||||
if config.Environments[config.ActiveEnvironment].OAuth.Redirect.Port == 0 {
|
|
||||||
return fmt.Errorf("missing OAuth Redirect Port configuration value")
|
|
||||||
}
|
|
||||||
if config.Environments[config.ActiveEnvironment].OAuth.TokenUrl == "" {
|
|
||||||
return fmt.Errorf("missing OAuth TokenUrl configuration value")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid authtype '%s' configured", config.AuthType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Environment struct {
|
|
||||||
Pat PatConfig `mapstructure:"pat"`
|
|
||||||
OAuth OAuthConfig `mapstructure:"oauth"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenResponse struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
ExpiresIn int `json:"expires_in"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Redirect struct {
|
|
||||||
Port int `mapstructure:"port"`
|
|
||||||
Path string `mapstructure:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Token struct {
|
|
||||||
AccessToken string `mapstructure:"accesstoken"`
|
|
||||||
Expiry time.Time `mapstructure:"expiry"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OAuthConfig struct {
|
|
||||||
Tenant string `mapstructure:"tenant"`
|
|
||||||
AuthUrl string `mapstructure:"authurl"`
|
|
||||||
BaseUrl string `mapstructure:"baseurl"`
|
|
||||||
TokenUrl string `mapstructure:"tokenurl"`
|
|
||||||
Redirect Redirect `mapstructure:"redirect"`
|
|
||||||
ClientSecret string `mapstructure:"clientSecret"`
|
|
||||||
ClientID string `mapstructure:"clientid"`
|
|
||||||
Token Token `mapstructure:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PatConfig struct {
|
|
||||||
Tenant string `mapstructure:"tenant"`
|
|
||||||
BaseUrl string `mapstructure:"baseurl"`
|
|
||||||
TokenUrl string `mapstructure:"tokenurl"`
|
|
||||||
ClientSecret string `mapstructure:"clientSecret"`
|
|
||||||
ClientID string `mapstructure:"clientid"`
|
|
||||||
Token Token `mapstructure:"token"`
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
sailpointbetasdk "github.com/sailpoint-oss/golang-sdk/sdk-output/beta"
|
|
||||||
sailpointsdk "github.com/sailpoint-oss/golang-sdk/sdk-output/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Template interface {
|
|
||||||
GetName() string
|
|
||||||
GetDescription() string
|
|
||||||
GetVariableCount() int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Variable struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchTemplate struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Variables []Variable `json:"variables"`
|
|
||||||
SearchQuery sailpointsdk.Search1 `json:"searchQuery"`
|
|
||||||
Raw []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (template SearchTemplate) GetName() string {
|
|
||||||
return template.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (template SearchTemplate) GetDescription() string {
|
|
||||||
return template.Description
|
|
||||||
}
|
|
||||||
|
|
||||||
func (template SearchTemplate) GetVariableCount() int {
|
|
||||||
return len(template.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExportTemplate struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Variables []Variable `json:"variables"`
|
|
||||||
ExportBody sailpointbetasdk.ExportPayload `json:"exportBody"`
|
|
||||||
Raw []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (template ExportTemplate) GetName() string {
|
|
||||||
return template.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (template ExportTemplate) GetDescription() string {
|
|
||||||
return template.Description
|
|
||||||
}
|
|
||||||
|
|
||||||
func (template ExportTemplate) GetVariableCount() int {
|
|
||||||
return len(template.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Templates struct {
|
|
||||||
Templates []SearchTemplate `json:"templates"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Choice struct {
|
|
||||||
Title string
|
|
||||||
Description string
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// InputPrompt receives a string value using the label
|
|
||||||
func InputPrompt(label string) string {
|
|
||||||
var s string
|
|
||||||
r := bufio.NewReader(os.Stdin)
|
|
||||||
for {
|
|
||||||
fmt.Fprint(os.Stderr, label+" ")
|
|
||||||
s, _ = r.ReadString('\n')
|
|
||||||
if s != "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(s)
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
|
||||||
sailpointsdk "github.com/sailpoint-oss/golang-sdk/sdk-output/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseIndicie(indicie string) (sailpointsdk.Index, error) {
|
|
||||||
switch indicie {
|
|
||||||
case "accessprofiles":
|
|
||||||
return sailpointsdk.INDEX_ACCESSPROFILES, nil
|
|
||||||
case "accountactivities":
|
|
||||||
return sailpointsdk.INDEX_ACCOUNTACTIVITIES, nil
|
|
||||||
case "entitlements":
|
|
||||||
return sailpointsdk.INDEX_ENTITLEMENTS, nil
|
|
||||||
case "events":
|
|
||||||
return sailpointsdk.INDEX_EVENTS, nil
|
|
||||||
case "identities":
|
|
||||||
return sailpointsdk.INDEX_IDENTITIES, nil
|
|
||||||
case "roles":
|
|
||||||
return sailpointsdk.INDEX_ROLES, nil
|
|
||||||
}
|
|
||||||
return "*", fmt.Errorf("indicie provided is invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildSearch(searchQuery string, sort []string, indicies []string) (sailpointsdk.Search1, error) {
|
|
||||||
|
|
||||||
search := sailpointsdk.NewSearch1()
|
|
||||||
search.Query = sailpointsdk.NewQuery()
|
|
||||||
search.Query.Query = &searchQuery
|
|
||||||
search.Sort = sort
|
|
||||||
search.Indices = []sailpointsdk.Index{}
|
|
||||||
|
|
||||||
for i := 0; i < len(indicies); i++ {
|
|
||||||
tempIndicie, err := ParseIndicie(indicies[i])
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return *search, err
|
|
||||||
}
|
|
||||||
|
|
||||||
search.Indices = append(search.Indices, tempIndicie)
|
|
||||||
}
|
|
||||||
|
|
||||||
return *search, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PerformSearch(apiClient sailpoint.APIClient, search sailpointsdk.Search1) ([]byte, error) {
|
|
||||||
|
|
||||||
ctx := context.TODO()
|
|
||||||
resp, r, err := sailpoint.PaginateWithDefaults[map[string]interface{}](apiClient.V3.SearchApi.SearchPost(ctx).Search1(search))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
||||||
fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r)
|
|
||||||
}
|
|
||||||
|
|
||||||
color.Green("Search complete, saving results")
|
|
||||||
formatted, err := json.MarshalIndent(resp, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveResults(formattedResponse []byte, fileName string, output string) error {
|
|
||||||
savePath := path.Join(output, fileName)
|
|
||||||
|
|
||||||
// Make sure the output dir exists first
|
|
||||||
err := os.MkdirAll(output, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.OpenFile(savePath, os.O_CREATE|os.O_RDWR, 0777)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fileWriter := bufio.NewWriter(file)
|
|
||||||
|
|
||||||
_, err = fileWriter.Write(formattedResponse)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
color.Green("Search Results saved to %s", savePath)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
sailpointbetasdk "github.com/sailpoint-oss/golang-sdk/sdk-output/beta"
|
|
||||||
)
|
|
||||||
|
|
||||||
func PrintJob(job sailpointbetasdk.SpConfigJob) {
|
|
||||||
fmt.Printf("Job Type: %s\nJob ID: %s\nStatus: %s\nExpired: %s\nCreated: %s\nModified: %s\nCompleted: %s\n", job.Type, job.JobId, job.Status, job.GetExpiration(), job.GetCreated(), job.GetModified(), job.GetCompleted())
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/tui"
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/types"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetSearchTemplates() ([]types.SearchTemplate, error) {
|
|
||||||
var searchTemplates []types.SearchTemplate
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
templateFiles := []string{filepath.Join(home, ".sailpoint", "search-templates.json")}
|
|
||||||
|
|
||||||
customTemplates := viper.GetString("customSearchTemplatesPath")
|
|
||||||
if customTemplates != "" {
|
|
||||||
templateFiles = append(templateFiles, customTemplates)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(templateFiles); i++ {
|
|
||||||
var templates []types.SearchTemplate
|
|
||||||
templateFile := templateFiles[i]
|
|
||||||
|
|
||||||
file, err := os.OpenFile(templateFile, os.O_CREATE|os.O_RDWR, 0777)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := io.ReadAll(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(raw, &templates)
|
|
||||||
if err != nil {
|
|
||||||
color.Red("an error occured while parsing the file: %s", templateFile)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
searchTemplates = append(searchTemplates, templates...)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(searchTemplates); i++ {
|
|
||||||
entry := &searchTemplates[i]
|
|
||||||
if len(entry.Variables) > 0 {
|
|
||||||
entry.Raw, err = json.Marshal(entry.SearchQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return searchTemplates, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetExportTemplates() ([]types.ExportTemplate, error) {
|
|
||||||
var exportTemplates []types.ExportTemplate
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
templateFiles := []string{filepath.Join(home, ".sailpoint", "export-templates.json")}
|
|
||||||
|
|
||||||
customTemplates := viper.GetString("customExportTemplatesPath")
|
|
||||||
if customTemplates != "" {
|
|
||||||
templateFiles = append(templateFiles, customTemplates)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(templateFiles); i++ {
|
|
||||||
var templates []types.ExportTemplate
|
|
||||||
templateFile := templateFiles[i]
|
|
||||||
|
|
||||||
file, err := os.OpenFile(templateFile, os.O_CREATE|os.O_RDWR, 0777)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := io.ReadAll(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(raw, &templates)
|
|
||||||
if err != nil {
|
|
||||||
color.Red("an error occured while parsing the file: %s", templateFile)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
exportTemplates = append(exportTemplates, templates...)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(exportTemplates); i++ {
|
|
||||||
entry := &exportTemplates[i]
|
|
||||||
if len(entry.Variables) > 0 {
|
|
||||||
entry.Raw, err = json.Marshal(entry.ExportBody)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return exportTemplates, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SelectTemplate[T types.Template](templates []T) (string, error) {
|
|
||||||
var prompts []types.Choice
|
|
||||||
for i := 0; i < len(templates); i++ {
|
|
||||||
temp := templates[i]
|
|
||||||
|
|
||||||
var description string
|
|
||||||
if temp.GetVariableCount() > 0 {
|
|
||||||
description = fmt.Sprintf("%s - Accepts Input", temp.GetDescription())
|
|
||||||
} else {
|
|
||||||
description = temp.GetDescription()
|
|
||||||
}
|
|
||||||
prompts = append(prompts, types.Choice{Title: temp.GetName(), Description: description})
|
|
||||||
}
|
|
||||||
|
|
||||||
intermediate, err := tui.PromptList(prompts, "Select a Template")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return intermediate.Title, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// Copyright (c) 2023, SailPoint Technologies, Inc. All rights reserved.
|
|
||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PromptPassword prompts user to enter password and then returns it
|
|
||||||
func PromptPassword(promptMsg string) (string, error) {
|
|
||||||
fmt.Print(promptMsg)
|
|
||||||
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
return strings.TrimSpace(string(bytePassword)), nil
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
// Copyright (c) 2023, SailPoint Technologies, Inc. All rights reserved.
|
|
||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/pkg/sftp"
|
|
||||||
"github.com/vbauerster/mpb/v8"
|
|
||||||
"github.com/vbauerster/mpb/v8/decor"
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RunVACmd(addr string, password string, cmd string) (string, error) {
|
|
||||||
config := &ssh.ClientConfig{
|
|
||||||
User: "sailpoint",
|
|
||||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
||||||
Auth: []ssh.AuthMethod{
|
|
||||||
ssh.Password(password),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Connect
|
|
||||||
client, dialErr := ssh.Dial("tcp", net.JoinHostPort(addr, "22"), config)
|
|
||||||
if dialErr != nil {
|
|
||||||
return "", dialErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a session. It is one session per command.
|
|
||||||
session, sessionErr := client.NewSession()
|
|
||||||
if sessionErr != nil {
|
|
||||||
return "", sessionErr
|
|
||||||
}
|
|
||||||
defer session.Close()
|
|
||||||
|
|
||||||
// import "bytes"
|
|
||||||
var b bytes.Buffer
|
|
||||||
|
|
||||||
// get output
|
|
||||||
session.Stdout = &b
|
|
||||||
|
|
||||||
// Finally, run the command
|
|
||||||
runErr := session.Run(cmd)
|
|
||||||
if runErr != nil {
|
|
||||||
return b.String(), runErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the output
|
|
||||||
return b.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func RunVACmdLive(addr string, password string, cmd string) error {
|
|
||||||
config := &ssh.ClientConfig{
|
|
||||||
User: "sailpoint",
|
|
||||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
||||||
Auth: []ssh.AuthMethod{
|
|
||||||
ssh.Password(password),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Connect
|
|
||||||
client, dialErr := ssh.Dial("tcp", net.JoinHostPort(addr, "22"), config)
|
|
||||||
if dialErr != nil {
|
|
||||||
return dialErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a session. It is one session per command.
|
|
||||||
session, sessionErr := client.NewSession()
|
|
||||||
if sessionErr != nil {
|
|
||||||
return sessionErr
|
|
||||||
}
|
|
||||||
defer session.Close()
|
|
||||||
|
|
||||||
// get output
|
|
||||||
session.Stdout = os.Stdout
|
|
||||||
|
|
||||||
// Finally, run the command
|
|
||||||
runErr := session.Run(cmd)
|
|
||||||
if runErr != nil {
|
|
||||||
return runErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the output
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CollectVAFiles(endpoint string, password string, output string, files []string) error {
|
|
||||||
color.Blue("Starting File Collection for %s\n", endpoint)
|
|
||||||
config := &ssh.ClientConfig{
|
|
||||||
User: "sailpoint",
|
|
||||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
||||||
Auth: []ssh.AuthMethod{
|
|
||||||
ssh.Password(password),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
// passed wg will be accounted at p.Wait() call
|
|
||||||
p := mpb.New(mpb.WithWidth(60),
|
|
||||||
mpb.PopCompletedMode(),
|
|
||||||
mpb.WithRefreshRate(180*time.Millisecond),
|
|
||||||
mpb.WithWaitGroup(&wg))
|
|
||||||
|
|
||||||
log.SetOutput(p)
|
|
||||||
|
|
||||||
outputFolder := path.Join(output, endpoint)
|
|
||||||
|
|
||||||
for i := 0; i < len(files); i++ {
|
|
||||||
filePath := files[i]
|
|
||||||
wg.Add(1)
|
|
||||||
go func(filePath string) error {
|
|
||||||
// Connect
|
|
||||||
client, err := ssh.Dial("tcp", net.JoinHostPort(endpoint, "22"), config)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sftp, err := sftp.NewClient(client)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
defer sftp.Close()
|
|
||||||
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
_, base := path.Split(filePath)
|
|
||||||
outputFile := path.Join(outputFolder, base)
|
|
||||||
if _, err := os.Stat(outputFolder); errors.Is(err, os.ErrNotExist) {
|
|
||||||
err := os.MkdirAll(outputFolder, 0700)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remoteFileStats, statErr := sftp.Stat(filePath)
|
|
||||||
if statErr == nil {
|
|
||||||
name := fmt.Sprintf("%v - %v", endpoint, base)
|
|
||||||
bar := p.AddBar(remoteFileStats.Size(),
|
|
||||||
mpb.BarFillerClearOnComplete(),
|
|
||||||
mpb.PrependDecorators(
|
|
||||||
// simple name decorator
|
|
||||||
decor.Name(name, decor.WCSyncSpaceR),
|
|
||||||
decor.Name(":", decor.WCSyncSpaceR),
|
|
||||||
decor.OnComplete(decor.CountersKiloByte("% .2f / % .2f", decor.WCSyncSpaceR), "Complete"),
|
|
||||||
decor.TotalKiloByte("% .2f", decor.WCSyncSpaceR),
|
|
||||||
),
|
|
||||||
mpb.AppendDecorators(
|
|
||||||
decor.OnComplete(decor.EwmaSpeed(decor.UnitKB, "% .2f", 90, decor.WCSyncWidth), ""),
|
|
||||||
decor.OnComplete(decor.Percentage(decor.WC{W: 5}), ""),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Open the source file
|
|
||||||
srcFile, err := sftp.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
defer srcFile.Close()
|
|
||||||
|
|
||||||
// Create the destination file
|
|
||||||
dstFile, err := os.Create(outputFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dstFile.Close()
|
|
||||||
|
|
||||||
writer := io.Writer(dstFile)
|
|
||||||
|
|
||||||
// create proxy reader
|
|
||||||
proxyWriter := bar.ProxyWriter(writer)
|
|
||||||
defer proxyWriter.Close()
|
|
||||||
|
|
||||||
io.Copy(proxyWriter, srcFile)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}(filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Wait()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
66
main.go
66
main.go
@@ -2,72 +2,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/root"
|
"github.com/sailpoint-oss/sailpoint-cli/cmd/root"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||||
"github.com/sailpoint-oss/sailpoint-cli/internal/types"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var rootCmd *cobra.Command
|
||||||
c client.Client
|
|
||||||
rootCmd *cobra.Command
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var Config types.CLIConfig
|
config.InitConfig()
|
||||||
var DevNull types.DevNull
|
rootCmd = root.NewRootCmd()
|
||||||
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
cobra.CheckErr(err)
|
|
||||||
|
|
||||||
viper.AddConfigPath(filepath.Join(home, ".sailpoint"))
|
|
||||||
viper.SetConfigName("config")
|
|
||||||
viper.SetConfigType("yaml")
|
|
||||||
viper.SetEnvPrefix("sail")
|
|
||||||
|
|
||||||
viper.AutomaticEnv()
|
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
|
||||||
// Config file not found; ignore error if desired
|
|
||||||
// IGNORE they may be using env vars
|
|
||||||
} else {
|
|
||||||
// Config file was found but another error was produced
|
|
||||||
cobra.CheckErr(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = viper.Unmarshal(&Config)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("unable to decode config: %s ", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
Config.EnsureAccessToken()
|
|
||||||
|
|
||||||
BaseUrl, err := Config.GetBaseUrl()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("unable to retrieve baseURL: %s ", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
Token, err := Config.GetAuthToken()
|
|
||||||
fmt.Println(Token)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("unable to retrieve accesstoken: %s ", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
configuration := sailpoint.NewConfiguration(sailpoint.ClientConfiguration{Token: Token, BaseURL: BaseUrl})
|
|
||||||
apiClient := sailpoint.NewAPIClient(configuration)
|
|
||||||
apiClient.V3.GetConfig().HTTPClient.Logger = DevNull
|
|
||||||
apiClient.Beta.GetConfig().HTTPClient.Logger = DevNull
|
|
||||||
|
|
||||||
rootCmd = root.NewRootCmd(c, apiClient)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// main the entry point for commands. Note that we do not need to do cobra.CheckErr(err)
|
// main the entry point for commands. Note that we do not need to do cobra.CheckErr(err)
|
||||||
@@ -75,7 +21,9 @@ func init() {
|
|||||||
// cause error messages to be logged twice. We do need to exit with error code if something
|
// cause error messages to be logged twice. We do need to exit with error code if something
|
||||||
// goes wrong. This will exit the cli container during pipeline build and fail that stage.
|
// goes wrong. This will exit the cli container during pipeline build and fail that stage.
|
||||||
func main() {
|
func main() {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
err := rootCmd.Execute()
|
||||||
|
config.SaveConfig()
|
||||||
|
if err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
vendor/github.com/gocarina/gocsv/.gitignore
generated
vendored
Normal file
1
vendor/github.com/gocarina/gocsv/.gitignore
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.idea
|
||||||
4
vendor/github.com/gocarina/gocsv/.travis.yml
generated
vendored
Normal file
4
vendor/github.com/gocarina/gocsv/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
language: go
|
||||||
|
arch:
|
||||||
|
- amd64
|
||||||
|
- ppc64le
|
||||||
21
vendor/github.com/gocarina/gocsv/LICENSE
generated
vendored
Normal file
21
vendor/github.com/gocarina/gocsv/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Jonathan Picques
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
173
vendor/github.com/gocarina/gocsv/README.md
generated
vendored
Normal file
173
vendor/github.com/gocarina/gocsv/README.md
generated
vendored
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
Go CSV
|
||||||
|
=====
|
||||||
|
|
||||||
|
The GoCSV package aims to provide easy serialization and deserialization functions to use CSV in Golang
|
||||||
|
|
||||||
|
API and techniques inspired from https://godoc.org/gopkg.in/mgo.v2
|
||||||
|
|
||||||
|
[](https://godoc.org/github.com/gocarina/gocsv)
|
||||||
|
[](https://travis-ci.org/gocarina/gocsv)
|
||||||
|
|
||||||
|
Installation
|
||||||
|
=====
|
||||||
|
|
||||||
|
```go get -u github.com/gocarina/gocsv```
|
||||||
|
|
||||||
|
Full example
|
||||||
|
=====
|
||||||
|
|
||||||
|
Consider the following CSV file
|
||||||
|
|
||||||
|
```csv
|
||||||
|
|
||||||
|
client_id,client_name,client_age
|
||||||
|
1,Jose,42
|
||||||
|
2,Daniel,26
|
||||||
|
3,Vincent,32
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Easy binding in Go!
|
||||||
|
---
|
||||||
|
|
||||||
|
```go
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gocarina/gocsv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotUsed struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct { // Our example struct, you can use "-" to ignore a field
|
||||||
|
Id string `csv:"client_id"`
|
||||||
|
Name string `csv:"client_name"`
|
||||||
|
Age string `csv:"client_age"`
|
||||||
|
NotUsedString string `csv:"-"`
|
||||||
|
NotUsedStruct NotUsed `csv:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
clientsFile, err := os.OpenFile("clients.csv", os.O_RDWR|os.O_CREATE, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer clientsFile.Close()
|
||||||
|
|
||||||
|
clients := []*Client{}
|
||||||
|
|
||||||
|
if err := gocsv.UnmarshalFile(clientsFile, &clients); err != nil { // Load clients from file
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
fmt.Println("Hello", client.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := clientsFile.Seek(0, 0); err != nil { // Go to the start of the file
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clients = append(clients, &Client{Id: "12", Name: "John", Age: "21"}) // Add clients
|
||||||
|
clients = append(clients, &Client{Id: "13", Name: "Fred"})
|
||||||
|
clients = append(clients, &Client{Id: "14", Name: "James", Age: "32"})
|
||||||
|
clients = append(clients, &Client{Id: "15", Name: "Danny"})
|
||||||
|
csvContent, err := gocsv.MarshalString(&clients) // Get all clients as CSV string
|
||||||
|
//err = gocsv.MarshalFile(&clients, clientsFile) // Use this to save the CSV back to the file
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(csvContent) // Display all clients as CSV string
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Customizable Converters
|
||||||
|
---
|
||||||
|
|
||||||
|
```go
|
||||||
|
|
||||||
|
type DateTime struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the internal date as CSV string
|
||||||
|
func (date *DateTime) MarshalCSV() (string, error) {
|
||||||
|
return date.Time.Format("20060201"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// You could also use the standard Stringer interface
|
||||||
|
func (date *DateTime) String() (string) {
|
||||||
|
return date.String() // Redundant, just for example
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the CSV string as internal date
|
||||||
|
func (date *DateTime) UnmarshalCSV(csv string) (err error) {
|
||||||
|
date.Time, err = time.Parse("20060201", csv)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct { // Our example struct with a custom type (DateTime)
|
||||||
|
Id string `csv:"id"`
|
||||||
|
Name string `csv:"name"`
|
||||||
|
Employed DateTime `csv:"employed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Customizable CSV Reader / Writer
|
||||||
|
---
|
||||||
|
|
||||||
|
```go
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
...
|
||||||
|
|
||||||
|
gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
|
||||||
|
r := csv.NewReader(in)
|
||||||
|
r.Comma = '|'
|
||||||
|
return r // Allows use pipe as delimiter
|
||||||
|
})
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
|
||||||
|
r := csv.NewReader(in)
|
||||||
|
r.LazyQuotes = true
|
||||||
|
r.Comma = '.'
|
||||||
|
return r // Allows use dot as delimiter and use quotes in CSV
|
||||||
|
})
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
|
||||||
|
//return csv.NewReader(in)
|
||||||
|
return gocsv.LazyCSVReader(in) // Allows use of quotes in CSV
|
||||||
|
})
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
gocsv.UnmarshalFile(file, &clients)
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
gocsv.SetCSVWriter(func(out io.Writer) *gocsv.SafeCSVWriter {
|
||||||
|
writer := csv.NewWriter(out)
|
||||||
|
writer.Comma = '|'
|
||||||
|
return gocsv.NewSafeCSVWriter(writer)
|
||||||
|
})
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
gocsv.MarshalFile(&clients, file)
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
537
vendor/github.com/gocarina/gocsv/csv.go
generated
vendored
Normal file
537
vendor/github.com/gocarina/gocsv/csv.go
generated
vendored
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
// Copyright 2014 Jonathan Picques. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license
|
||||||
|
// The license can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// The GoCSV package aims to provide easy CSV serialization and deserialization to the golang programming language
|
||||||
|
|
||||||
|
package gocsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FailIfUnmatchedStructTags indicates whether it is considered an error when there is an unmatched
|
||||||
|
// struct tag.
|
||||||
|
var FailIfUnmatchedStructTags = false
|
||||||
|
|
||||||
|
// FailIfDoubleHeaderNames indicates whether it is considered an error when a header name is repeated
|
||||||
|
// in the csv header.
|
||||||
|
var FailIfDoubleHeaderNames = false
|
||||||
|
|
||||||
|
// ShouldAlignDuplicateHeadersWithStructFieldOrder indicates whether we should align duplicate CSV
|
||||||
|
// headers per their alignment in the struct definition.
|
||||||
|
var ShouldAlignDuplicateHeadersWithStructFieldOrder = false
|
||||||
|
|
||||||
|
// TagName defines key in the struct field's tag to scan
|
||||||
|
var TagName = "csv"
|
||||||
|
|
||||||
|
// TagSeparator defines seperator string for multiple csv tags in struct fields
|
||||||
|
var TagSeparator = ","
|
||||||
|
|
||||||
|
// Normalizer is a function that takes and returns a string. It is applied to
|
||||||
|
// struct and header field values before they are compared. It can be used to alter
|
||||||
|
// names for comparison. For instance, you could allow case insensitive matching
|
||||||
|
// or convert '-' to '_'.
|
||||||
|
type Normalizer func(string) string
|
||||||
|
|
||||||
|
type ErrorHandler func(*csv.ParseError) bool
|
||||||
|
|
||||||
|
// normalizeName function initially set to a nop Normalizer.
|
||||||
|
var normalizeName = DefaultNameNormalizer()
|
||||||
|
|
||||||
|
// DefaultNameNormalizer is a nop Normalizer.
|
||||||
|
func DefaultNameNormalizer() Normalizer { return func(s string) string { return s } }
|
||||||
|
|
||||||
|
// SetHeaderNormalizer sets the normalizer used to normalize struct and header field names.
|
||||||
|
func SetHeaderNormalizer(f Normalizer) {
|
||||||
|
normalizeName = f
|
||||||
|
// Need to clear the cache hen the header normalizer changes.
|
||||||
|
structInfoCache = sync.Map{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// CSVWriter used to format CSV
|
||||||
|
|
||||||
|
var selfCSVWriter = DefaultCSVWriter
|
||||||
|
|
||||||
|
// DefaultCSVWriter is the default SafeCSVWriter used to format CSV (cf. csv.NewWriter)
|
||||||
|
func DefaultCSVWriter(out io.Writer) *SafeCSVWriter {
|
||||||
|
writer := NewSafeCSVWriter(csv.NewWriter(out))
|
||||||
|
|
||||||
|
// As only one rune can be defined as a CSV separator, we are going to trim
|
||||||
|
// the custom tag separator and use the first rune.
|
||||||
|
if runes := []rune(strings.TrimSpace(TagSeparator)); len(runes) > 0 {
|
||||||
|
writer.Comma = runes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCSVWriter sets the SafeCSVWriter used to format CSV.
|
||||||
|
func SetCSVWriter(csvWriter func(io.Writer) *SafeCSVWriter) {
|
||||||
|
selfCSVWriter = csvWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCSVWriter(out io.Writer) *SafeCSVWriter {
|
||||||
|
return selfCSVWriter(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// CSVReader used to parse CSV
|
||||||
|
|
||||||
|
var selfCSVReader = DefaultCSVReader
|
||||||
|
|
||||||
|
// DefaultCSVReader is the default CSV reader used to parse CSV (cf. csv.NewReader)
|
||||||
|
func DefaultCSVReader(in io.Reader) CSVReader {
|
||||||
|
return csv.NewReader(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LazyCSVReader returns a lazy CSV reader, with LazyQuotes and TrimLeadingSpace.
|
||||||
|
func LazyCSVReader(in io.Reader) CSVReader {
|
||||||
|
csvReader := csv.NewReader(in)
|
||||||
|
csvReader.LazyQuotes = true
|
||||||
|
csvReader.TrimLeadingSpace = true
|
||||||
|
return csvReader
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCSVReader sets the CSV reader used to parse CSV.
|
||||||
|
func SetCSVReader(csvReader func(io.Reader) CSVReader) {
|
||||||
|
selfCSVReader = csvReader
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCSVReader(in io.Reader) CSVReader {
|
||||||
|
return selfCSVReader(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Marshal functions
|
||||||
|
|
||||||
|
// MarshalFile saves the interface as CSV in the file.
|
||||||
|
func MarshalFile(in interface{}, file *os.File) (err error) {
|
||||||
|
return Marshal(in, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalString returns the CSV string from the interface.
|
||||||
|
func MarshalString(in interface{}) (out string, err error) {
|
||||||
|
bufferString := bytes.NewBufferString(out)
|
||||||
|
if err := Marshal(in, bufferString); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return bufferString.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalStringWithoutHeaders returns the CSV string from the interface.
|
||||||
|
func MarshalStringWithoutHeaders(in interface{}) (out string, err error) {
|
||||||
|
bufferString := bytes.NewBufferString(out)
|
||||||
|
if err := MarshalWithoutHeaders(in, bufferString); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return bufferString.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBytes returns the CSV bytes from the interface.
|
||||||
|
func MarshalBytes(in interface{}) (out []byte, err error) {
|
||||||
|
bufferString := bytes.NewBuffer(out)
|
||||||
|
if err := Marshal(in, bufferString); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bufferString.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal returns the CSV in writer from the interface.
|
||||||
|
func Marshal(in interface{}, out io.Writer) (err error) {
|
||||||
|
writer := getCSVWriter(out)
|
||||||
|
return writeTo(writer, in, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalWithoutHeaders returns the CSV in writer from the interface.
|
||||||
|
func MarshalWithoutHeaders(in interface{}, out io.Writer) (err error) {
|
||||||
|
writer := getCSVWriter(out)
|
||||||
|
return writeTo(writer, in, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalChan returns the CSV read from the channel.
|
||||||
|
func MarshalChan(c <-chan interface{}, out CSVWriter) error {
|
||||||
|
return writeFromChan(out, c, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalChanWithoutHeaders returns the CSV read from the channel.
|
||||||
|
func MarshalChanWithoutHeaders(c <-chan interface{}, out CSVWriter) error {
|
||||||
|
return writeFromChan(out, c, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalCSV returns the CSV in writer from the interface.
|
||||||
|
func MarshalCSV(in interface{}, out CSVWriter) (err error) {
|
||||||
|
return writeTo(out, in, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalCSVWithoutHeaders returns the CSV in writer from the interface.
|
||||||
|
func MarshalCSVWithoutHeaders(in interface{}, out CSVWriter) (err error) {
|
||||||
|
return writeTo(out, in, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Unmarshal functions
|
||||||
|
|
||||||
|
// UnmarshalFile parses the CSV from the file in the interface.
|
||||||
|
func UnmarshalFile(in *os.File, out interface{}) error {
|
||||||
|
return Unmarshal(in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalFile parses the CSV from the file in the interface.
|
||||||
|
func UnmarshalFileWithErrorHandler(in *os.File, errHandler ErrorHandler, out interface{}) error {
|
||||||
|
return UnmarshalWithErrorHandler(in, errHandler, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalString parses the CSV from the string in the interface.
|
||||||
|
func UnmarshalString(in string, out interface{}) error {
|
||||||
|
return Unmarshal(strings.NewReader(in), out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBytes parses the CSV from the bytes in the interface.
|
||||||
|
func UnmarshalBytes(in []byte, out interface{}) error {
|
||||||
|
return Unmarshal(bytes.NewReader(in), out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal parses the CSV from the reader in the interface.
|
||||||
|
func Unmarshal(in io.Reader, out interface{}) error {
|
||||||
|
return readTo(newSimpleDecoderFromReader(in), out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal parses the CSV from the reader in the interface.
|
||||||
|
func UnmarshalWithErrorHandler(in io.Reader, errHandle ErrorHandler, out interface{}) error {
|
||||||
|
return readToWithErrorHandler(newSimpleDecoderFromReader(in), errHandle, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalWithoutHeaders parses the CSV from the reader in the interface.
|
||||||
|
func UnmarshalWithoutHeaders(in io.Reader, out interface{}) error {
|
||||||
|
return readToWithoutHeaders(newSimpleDecoderFromReader(in), out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCSVWithoutHeaders parses a headerless CSV with passed in CSV reader
|
||||||
|
func UnmarshalCSVWithoutHeaders(in CSVReader, out interface{}) error {
|
||||||
|
return readToWithoutHeaders(csvDecoder{in}, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalDecoder parses the CSV from the decoder in the interface
|
||||||
|
func UnmarshalDecoder(in Decoder, out interface{}) error {
|
||||||
|
return readTo(in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCSV parses the CSV from the reader in the interface.
|
||||||
|
func UnmarshalCSV(in CSVReader, out interface{}) error {
|
||||||
|
return readTo(csvDecoder{in}, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCSVToMap parses a CSV of 2 columns into a map.
|
||||||
|
func UnmarshalCSVToMap(in CSVReader, out interface{}) error {
|
||||||
|
decoder := NewSimpleDecoderFromCSVReader(in)
|
||||||
|
header, err := decoder.GetCSVRow()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(header) != 2 {
|
||||||
|
return fmt.Errorf("maps can only be created for csv of two columns")
|
||||||
|
}
|
||||||
|
outValue, outType := getConcreteReflectValueAndType(out)
|
||||||
|
if outType.Kind() != reflect.Map {
|
||||||
|
return fmt.Errorf("cannot use " + outType.String() + ", only map supported")
|
||||||
|
}
|
||||||
|
keyType := outType.Key()
|
||||||
|
valueType := outType.Elem()
|
||||||
|
outValue.Set(reflect.MakeMap(outType))
|
||||||
|
for {
|
||||||
|
key := reflect.New(keyType)
|
||||||
|
value := reflect.New(valueType)
|
||||||
|
line, err := decoder.GetCSVRow()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := setField(key, line[0], false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := setField(value, line[1], false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outValue.SetMapIndex(key.Elem(), value.Elem())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalToChan parses the CSV from the reader and send each value in the chan c.
|
||||||
|
// The channel must have a concrete type.
|
||||||
|
func UnmarshalToChan(in io.Reader, c interface{}) error {
|
||||||
|
if c == nil {
|
||||||
|
return fmt.Errorf("goscv: channel is %v", c)
|
||||||
|
}
|
||||||
|
return readEach(newSimpleDecoderFromReader(in), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalToChanWithoutHeaders parses the CSV from the reader and send each value in the chan c.
|
||||||
|
// The channel must have a concrete type.
|
||||||
|
func UnmarshalToChanWithoutHeaders(in io.Reader, c interface{}) error {
|
||||||
|
if c == nil {
|
||||||
|
return fmt.Errorf("goscv: channel is %v", c)
|
||||||
|
}
|
||||||
|
return readEachWithoutHeaders(newSimpleDecoderFromReader(in), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalDecoderToChan parses the CSV from the decoder and send each value in the chan c.
|
||||||
|
// The channel must have a concrete type.
|
||||||
|
func UnmarshalDecoderToChan(in SimpleDecoder, c interface{}) error {
|
||||||
|
if c == nil {
|
||||||
|
return fmt.Errorf("goscv: channel is %v", c)
|
||||||
|
}
|
||||||
|
return readEach(in, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalStringToChan parses the CSV from the string and send each value in the chan c.
|
||||||
|
// The channel must have a concrete type.
|
||||||
|
func UnmarshalStringToChan(in string, c interface{}) error {
|
||||||
|
return UnmarshalToChan(strings.NewReader(in), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBytesToChan parses the CSV from the bytes and send each value in the chan c.
|
||||||
|
// The channel must have a concrete type.
|
||||||
|
func UnmarshalBytesToChan(in []byte, c interface{}) error {
|
||||||
|
return UnmarshalToChan(bytes.NewReader(in), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalToCallback parses the CSV from the reader and send each value to the given func f.
|
||||||
|
// The func must look like func(Struct).
|
||||||
|
func UnmarshalToCallback(in io.Reader, f interface{}) error {
|
||||||
|
valueFunc := reflect.ValueOf(f)
|
||||||
|
t := reflect.TypeOf(f)
|
||||||
|
if t.NumIn() != 1 {
|
||||||
|
return fmt.Errorf("the given function must have exactly one parameter")
|
||||||
|
}
|
||||||
|
cerr := make(chan error)
|
||||||
|
c := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, t.In(0)), 0)
|
||||||
|
go func() {
|
||||||
|
cerr <- UnmarshalToChan(in, c.Interface())
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-cerr:
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
v, notClosed := c.Recv()
|
||||||
|
if !notClosed || v.Interface() == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
callResults := valueFunc.Call([]reflect.Value{v})
|
||||||
|
// if last returned value from Call() is an error, return it
|
||||||
|
if len(callResults) > 0 {
|
||||||
|
if err, ok := callResults[len(callResults)-1].Interface().(error); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalDecoderToCallback parses the CSV from the decoder and send each value to the given func f.
|
||||||
|
// The func must look like func(Struct).
|
||||||
|
func UnmarshalDecoderToCallback(in SimpleDecoder, f interface{}) error {
|
||||||
|
valueFunc := reflect.ValueOf(f)
|
||||||
|
t := reflect.TypeOf(f)
|
||||||
|
if t.NumIn() != 1 {
|
||||||
|
return fmt.Errorf("the given function must have exactly one parameter")
|
||||||
|
}
|
||||||
|
cerr := make(chan error)
|
||||||
|
c := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, t.In(0)), 0)
|
||||||
|
go func() {
|
||||||
|
cerr <- UnmarshalDecoderToChan(in, c.Interface())
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-cerr:
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
v, notClosed := c.Recv()
|
||||||
|
if !notClosed || v.Interface() == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
valueFunc.Call([]reflect.Value{v})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBytesToCallback parses the CSV from the bytes and send each value to the given func f.
|
||||||
|
// The func must look like func(Struct).
|
||||||
|
func UnmarshalBytesToCallback(in []byte, f interface{}) error {
|
||||||
|
return UnmarshalToCallback(bytes.NewReader(in), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalStringToCallback parses the CSV from the string and send each value to the given func f.
|
||||||
|
// The func must look like func(Struct).
|
||||||
|
func UnmarshalStringToCallback(in string, c interface{}) (err error) {
|
||||||
|
return UnmarshalToCallback(strings.NewReader(in), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalToCallbackWithError parses the CSV from the reader and
|
||||||
|
// send each value to the given func f.
|
||||||
|
//
|
||||||
|
// If func returns error, it will stop processing, drain the
|
||||||
|
// parser and propagate the error to caller.
|
||||||
|
//
|
||||||
|
// The func must look like func(Struct) error.
|
||||||
|
func UnmarshalToCallbackWithError(in io.Reader, f interface{}) error {
|
||||||
|
valueFunc := reflect.ValueOf(f)
|
||||||
|
t := reflect.TypeOf(f)
|
||||||
|
if t.NumIn() != 1 {
|
||||||
|
return fmt.Errorf("the given function must have exactly one parameter")
|
||||||
|
}
|
||||||
|
if t.NumOut() != 1 {
|
||||||
|
return fmt.Errorf("the given function must have exactly one return value")
|
||||||
|
}
|
||||||
|
if !isErrorType(t.Out(0)) {
|
||||||
|
return fmt.Errorf("the given function must only return error")
|
||||||
|
}
|
||||||
|
|
||||||
|
cerr := make(chan error)
|
||||||
|
c := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, t.In(0)), 0)
|
||||||
|
go func() {
|
||||||
|
cerr <- UnmarshalToChan(in, c.Interface())
|
||||||
|
}()
|
||||||
|
|
||||||
|
var fErr error
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-cerr:
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fErr
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
v, notClosed := c.Recv()
|
||||||
|
if !notClosed || v.Interface() == nil {
|
||||||
|
if err := <-cerr; err != nil {
|
||||||
|
fErr = err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// callback f has already returned an error, stop processing but keep draining the chan c
|
||||||
|
if fErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
results := valueFunc.Call([]reflect.Value{v})
|
||||||
|
|
||||||
|
// If the callback f returns an error, stores it and returns it in future.
|
||||||
|
errValue := results[0].Interface()
|
||||||
|
if errValue != nil {
|
||||||
|
fErr = errValue.(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBytesToCallbackWithError parses the CSV from the bytes and
|
||||||
|
// send each value to the given func f.
|
||||||
|
//
|
||||||
|
// If func returns error, it will stop processing, drain the
|
||||||
|
// parser and propagate the error to caller.
|
||||||
|
//
|
||||||
|
// The func must look like func(Struct) error.
|
||||||
|
func UnmarshalBytesToCallbackWithError(in []byte, f interface{}) error {
|
||||||
|
return UnmarshalToCallbackWithError(bytes.NewReader(in), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalStringToCallbackWithError parses the CSV from the string and
|
||||||
|
// send each value to the given func f.
|
||||||
|
//
|
||||||
|
// If func returns error, it will stop processing, drain the
|
||||||
|
// parser and propagate the error to caller.
|
||||||
|
//
|
||||||
|
// The func must look like func(Struct) error.
|
||||||
|
func UnmarshalStringToCallbackWithError(in string, c interface{}) (err error) {
|
||||||
|
return UnmarshalToCallbackWithError(strings.NewReader(in), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSVToMap creates a simple map from a CSV of 2 columns.
|
||||||
|
func CSVToMap(in io.Reader) (map[string]string, error) {
|
||||||
|
decoder := newSimpleDecoderFromReader(in)
|
||||||
|
header, err := decoder.GetCSVRow()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(header) != 2 {
|
||||||
|
return nil, fmt.Errorf("maps can only be created for csv of two columns")
|
||||||
|
}
|
||||||
|
m := make(map[string]string)
|
||||||
|
for {
|
||||||
|
line, err := decoder.GetCSVRow()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m[line[0]] = line[1]
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSVToMaps takes a reader and returns an array of dictionaries, using the header row as the keys
|
||||||
|
func CSVToMaps(reader io.Reader) ([]map[string]string, error) {
|
||||||
|
r := csv.NewReader(reader)
|
||||||
|
rows := []map[string]string{}
|
||||||
|
var header []string
|
||||||
|
for {
|
||||||
|
record, err := r.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if header == nil {
|
||||||
|
header = record
|
||||||
|
} else {
|
||||||
|
dict := map[string]string{}
|
||||||
|
for i := range header {
|
||||||
|
dict[header[i]] = record[i]
|
||||||
|
}
|
||||||
|
rows = append(rows, dict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSVToChanMaps parses the CSV from the reader and send a dictionary in the chan c, using the header row as the keys.
|
||||||
|
func CSVToChanMaps(reader io.Reader, c chan<- map[string]string) error {
|
||||||
|
r := csv.NewReader(reader)
|
||||||
|
var header []string
|
||||||
|
for {
|
||||||
|
record, err := r.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if header == nil {
|
||||||
|
header = record
|
||||||
|
} else {
|
||||||
|
dict := map[string]string{}
|
||||||
|
for i := range header {
|
||||||
|
dict[header[i]] = record[i]
|
||||||
|
}
|
||||||
|
c <- dict
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
488
vendor/github.com/gocarina/gocsv/decode.go
generated
vendored
Normal file
488
vendor/github.com/gocarina/gocsv/decode.go
generated
vendored
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
package gocsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decoder .
|
||||||
|
type Decoder interface {
|
||||||
|
GetCSVRows() ([][]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimpleDecoder .
|
||||||
|
type SimpleDecoder interface {
|
||||||
|
GetCSVRow() ([]string, error)
|
||||||
|
GetCSVRows() ([][]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CSVReader interface {
|
||||||
|
Read() ([]string, error)
|
||||||
|
ReadAll() ([][]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type csvDecoder struct {
|
||||||
|
CSVReader
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSimpleDecoderFromReader(r io.Reader) SimpleDecoder {
|
||||||
|
return csvDecoder{getCSVReader(r)}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrEmptyCSVFile = errors.New("empty csv file given")
|
||||||
|
ErrNoStructTags = errors.New("no csv struct tags found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSimpleDecoderFromCSVReader creates a SimpleDecoder, which may be passed
|
||||||
|
// to the UnmarshalDecoder* family of functions, from a CSV reader. Note that
|
||||||
|
// encoding/csv.Reader implements CSVReader, so you can pass one of those
|
||||||
|
// directly here.
|
||||||
|
func NewSimpleDecoderFromCSVReader(r CSVReader) SimpleDecoder {
|
||||||
|
return csvDecoder{r}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c csvDecoder) GetCSVRows() ([][]string, error) {
|
||||||
|
return c.ReadAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c csvDecoder) GetCSVRow() ([]string, error) {
|
||||||
|
return c.Read()
|
||||||
|
}
|
||||||
|
|
||||||
|
func mismatchStructFields(structInfo []fieldInfo, headers []string) []string {
|
||||||
|
missing := make([]string, 0)
|
||||||
|
if len(structInfo) == 0 {
|
||||||
|
return missing
|
||||||
|
}
|
||||||
|
|
||||||
|
headerMap := make(map[string]struct{}, len(headers))
|
||||||
|
for idx := range headers {
|
||||||
|
headerMap[headers[idx]] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, info := range structInfo {
|
||||||
|
found := false
|
||||||
|
for _, key := range info.keys {
|
||||||
|
if _, ok := headerMap[key]; ok {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
missing = append(missing, info.keys...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return missing
|
||||||
|
}
|
||||||
|
|
||||||
|
func mismatchHeaderFields(structInfo []fieldInfo, headers []string) []string {
|
||||||
|
missing := make([]string, 0)
|
||||||
|
if len(headers) == 0 {
|
||||||
|
return missing
|
||||||
|
}
|
||||||
|
|
||||||
|
keyMap := make(map[string]struct{})
|
||||||
|
for _, info := range structInfo {
|
||||||
|
for _, key := range info.keys {
|
||||||
|
keyMap[key] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, header := range headers {
|
||||||
|
if _, ok := keyMap[header]; !ok {
|
||||||
|
missing = append(missing, header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return missing
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeMissingStructFields(structInfo []fieldInfo, headers []string) error {
|
||||||
|
missing := mismatchStructFields(structInfo, headers)
|
||||||
|
if len(missing) != 0 {
|
||||||
|
return fmt.Errorf("found unmatched struct field with tags %v", missing)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that no header name is repeated twice
|
||||||
|
func maybeDoubleHeaderNames(headers []string) error {
|
||||||
|
headerMap := make(map[string]bool, len(headers))
|
||||||
|
for _, v := range headers {
|
||||||
|
if _, ok := headerMap[v]; ok {
|
||||||
|
return fmt.Errorf("repeated header name: %v", v)
|
||||||
|
}
|
||||||
|
headerMap[v] = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply normalizer func to headers
|
||||||
|
func normalizeHeaders(headers []string) []string {
|
||||||
|
out := make([]string, len(headers))
|
||||||
|
for i, h := range headers {
|
||||||
|
out[i] = normalizeName(h)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func readTo(decoder Decoder, out interface{}) error {
|
||||||
|
return readToWithErrorHandler(decoder, nil, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readToWithErrorHandler(decoder Decoder, errHandler ErrorHandler, out interface{}) error {
|
||||||
|
outValue, outType := getConcreteReflectValueAndType(out) // Get the concrete type (not pointer) (Slice<?> or Array<?>)
|
||||||
|
if err := ensureOutType(outType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outInnerWasPointer, outInnerType := getConcreteContainerInnerType(outType) // Get the concrete inner type (not pointer) (Container<"?">)
|
||||||
|
if err := ensureOutInnerType(outInnerType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
csvRows, err := decoder.GetCSVRows() // Get the CSV csvRows
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(csvRows) == 0 {
|
||||||
|
return ErrEmptyCSVFile
|
||||||
|
}
|
||||||
|
if err := ensureOutCapacity(&outValue, len(csvRows)); err != nil { // Ensure the container is big enough to hold the CSV content
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outInnerStructInfo := getStructInfo(outInnerType) // Get the inner struct info to get CSV annotations
|
||||||
|
if len(outInnerStructInfo.Fields) == 0 {
|
||||||
|
return ErrNoStructTags
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := normalizeHeaders(csvRows[0])
|
||||||
|
body := csvRows[1:]
|
||||||
|
|
||||||
|
csvHeadersLabels := make(map[int]*fieldInfo, len(outInnerStructInfo.Fields)) // Used to store the correspondance header <-> position in CSV
|
||||||
|
|
||||||
|
headerCount := map[string]int{}
|
||||||
|
for i, csvColumnHeader := range headers {
|
||||||
|
curHeaderCount := headerCount[csvColumnHeader]
|
||||||
|
if fieldInfo := getCSVFieldPosition(csvColumnHeader, outInnerStructInfo, curHeaderCount); fieldInfo != nil {
|
||||||
|
csvHeadersLabels[i] = fieldInfo
|
||||||
|
if ShouldAlignDuplicateHeadersWithStructFieldOrder {
|
||||||
|
curHeaderCount++
|
||||||
|
headerCount[csvColumnHeader] = curHeaderCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if FailIfUnmatchedStructTags {
|
||||||
|
if err := maybeMissingStructFields(outInnerStructInfo.Fields, headers); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if FailIfDoubleHeaderNames {
|
||||||
|
if err := maybeDoubleHeaderNames(headers); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var withFieldsOK bool
|
||||||
|
var fieldTypeUnmarshallerWithKeys TypeUnmarshalCSVWithFields
|
||||||
|
|
||||||
|
for i, csvRow := range body {
|
||||||
|
objectIface := reflect.New(outValue.Index(i).Type()).Interface()
|
||||||
|
outInner := createNewOutInner(outInnerWasPointer, outInnerType)
|
||||||
|
for j, csvColumnContent := range csvRow {
|
||||||
|
if fieldInfo, ok := csvHeadersLabels[j]; ok { // Position found accordingly to header name
|
||||||
|
|
||||||
|
if outInner.CanInterface() {
|
||||||
|
fieldTypeUnmarshallerWithKeys, withFieldsOK = objectIface.(TypeUnmarshalCSVWithFields)
|
||||||
|
if withFieldsOK {
|
||||||
|
if err := fieldTypeUnmarshallerWithKeys.UnmarshalCSVWithFields(fieldInfo.getFirstKey(), csvColumnContent); err != nil {
|
||||||
|
parseError := csv.ParseError{
|
||||||
|
Line: i + 2, //add 2 to account for the header & 0-indexing of arrays
|
||||||
|
Column: j + 1,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
return &parseError
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value := csvColumnContent
|
||||||
|
if value == "" {
|
||||||
|
value = fieldInfo.defaultValue
|
||||||
|
}
|
||||||
|
if err := setInnerField(&outInner, outInnerWasPointer, fieldInfo.IndexChain, value, fieldInfo.omitEmpty); err != nil { // Set field of struct
|
||||||
|
parseError := csv.ParseError{
|
||||||
|
Line: i + 2, //add 2 to account for the header & 0-indexing of arrays
|
||||||
|
Column: j + 1,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
if errHandler == nil || !errHandler(&parseError) {
|
||||||
|
return &parseError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if withFieldsOK {
|
||||||
|
reflectedObject := reflect.ValueOf(objectIface)
|
||||||
|
outInner = reflectedObject.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
outValue.Index(i).Set(outInner)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readEach(decoder SimpleDecoder, c interface{}) error {
|
||||||
|
outValue, outType := getConcreteReflectValueAndType(c) // Get the concrete type (not pointer)
|
||||||
|
if outType.Kind() != reflect.Chan {
|
||||||
|
return fmt.Errorf("cannot use %v with type %s, only channel supported", c, outType)
|
||||||
|
}
|
||||||
|
defer outValue.Close()
|
||||||
|
|
||||||
|
headers, err := decoder.GetCSVRow()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
headers = normalizeHeaders(headers)
|
||||||
|
|
||||||
|
outInnerWasPointer, outInnerType := getConcreteContainerInnerType(outType) // Get the concrete inner type (not pointer) (Container<"?">)
|
||||||
|
if err := ensureOutInnerType(outInnerType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outInnerStructInfo := getStructInfo(outInnerType) // Get the inner struct info to get CSV annotations
|
||||||
|
if len(outInnerStructInfo.Fields) == 0 {
|
||||||
|
return ErrNoStructTags
|
||||||
|
}
|
||||||
|
csvHeadersLabels := make(map[int]*fieldInfo, len(outInnerStructInfo.Fields)) // Used to store the correspondance header <-> position in CSV
|
||||||
|
headerCount := map[string]int{}
|
||||||
|
for i, csvColumnHeader := range headers {
|
||||||
|
curHeaderCount := headerCount[csvColumnHeader]
|
||||||
|
if fieldInfo := getCSVFieldPosition(csvColumnHeader, outInnerStructInfo, curHeaderCount); fieldInfo != nil {
|
||||||
|
csvHeadersLabels[i] = fieldInfo
|
||||||
|
if ShouldAlignDuplicateHeadersWithStructFieldOrder {
|
||||||
|
curHeaderCount++
|
||||||
|
headerCount[csvColumnHeader] = curHeaderCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := maybeMissingStructFields(outInnerStructInfo.Fields, headers); err != nil {
|
||||||
|
if FailIfUnmatchedStructTags {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if FailIfDoubleHeaderNames {
|
||||||
|
if err := maybeDoubleHeaderNames(headers); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
for {
|
||||||
|
line, err := decoder.GetCSVRow()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outInner := createNewOutInner(outInnerWasPointer, outInnerType)
|
||||||
|
for j, csvColumnContent := range line {
|
||||||
|
if fieldInfo, ok := csvHeadersLabels[j]; ok { // Position found accordingly to header name
|
||||||
|
if err := setInnerField(&outInner, outInnerWasPointer, fieldInfo.IndexChain, csvColumnContent, fieldInfo.omitEmpty); err != nil { // Set field of struct
|
||||||
|
return &csv.ParseError{
|
||||||
|
Line: i + 2, //add 2 to account for the header & 0-indexing of arrays
|
||||||
|
Column: j + 1,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outValue.Send(outInner)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readEachWithoutHeaders(decoder SimpleDecoder, c interface{}) error {
|
||||||
|
outValue, outType := getConcreteReflectValueAndType(c) // Get the concrete type (not pointer) (Slice<?> or Array<?>)
|
||||||
|
if err := ensureOutType(outType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outValue.Close()
|
||||||
|
|
||||||
|
outInnerWasPointer, outInnerType := getConcreteContainerInnerType(outType) // Get the concrete inner type (not pointer) (Container<"?">)
|
||||||
|
if err := ensureOutInnerType(outInnerType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outInnerStructInfo := getStructInfo(outInnerType) // Get the inner struct info to get CSV annotations
|
||||||
|
if len(outInnerStructInfo.Fields) == 0 {
|
||||||
|
return ErrNoStructTags
|
||||||
|
}
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for {
|
||||||
|
line, err := decoder.GetCSVRow()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outInner := createNewOutInner(outInnerWasPointer, outInnerType)
|
||||||
|
for j, csvColumnContent := range line {
|
||||||
|
fieldInfo := outInnerStructInfo.Fields[j]
|
||||||
|
if err := setInnerField(&outInner, outInnerWasPointer, fieldInfo.IndexChain, csvColumnContent, fieldInfo.omitEmpty); err != nil { // Set field of struct
|
||||||
|
return &csv.ParseError{
|
||||||
|
Line: i + 2, //add 2 to account for the header & 0-indexing of arrays
|
||||||
|
Column: j + 1,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outValue.Send(outInner)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readToWithoutHeaders(decoder Decoder, out interface{}) error {
|
||||||
|
outValue, outType := getConcreteReflectValueAndType(out) // Get the concrete type (not pointer) (Slice<?> or Array<?>)
|
||||||
|
if err := ensureOutType(outType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outInnerWasPointer, outInnerType := getConcreteContainerInnerType(outType) // Get the concrete inner type (not pointer) (Container<"?">)
|
||||||
|
if err := ensureOutInnerType(outInnerType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
csvRows, err := decoder.GetCSVRows() // Get the CSV csvRows
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(csvRows) == 0 {
|
||||||
|
return ErrEmptyCSVFile
|
||||||
|
}
|
||||||
|
if err := ensureOutCapacity(&outValue, len(csvRows)+1); err != nil { // Ensure the container is big enough to hold the CSV content
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outInnerStructInfo := getStructInfo(outInnerType) // Get the inner struct info to get CSV annotations
|
||||||
|
if len(outInnerStructInfo.Fields) == 0 {
|
||||||
|
return ErrNoStructTags
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, csvRow := range csvRows {
|
||||||
|
outInner := createNewOutInner(outInnerWasPointer, outInnerType)
|
||||||
|
for j, csvColumnContent := range csvRow {
|
||||||
|
fieldInfo := outInnerStructInfo.Fields[j]
|
||||||
|
if err := setInnerField(&outInner, outInnerWasPointer, fieldInfo.IndexChain, csvColumnContent, fieldInfo.omitEmpty); err != nil { // Set field of struct
|
||||||
|
return &csv.ParseError{
|
||||||
|
Line: i + 1,
|
||||||
|
Column: j + 1,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outValue.Index(i).Set(outInner)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the outType is an array or a slice
|
||||||
|
func ensureOutType(outType reflect.Type) error {
|
||||||
|
switch outType.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Chan:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Array:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot use " + outType.String() + ", only slice or array supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the outInnerType is of type struct
|
||||||
|
func ensureOutInnerType(outInnerType reflect.Type) error {
|
||||||
|
switch outInnerType.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot use " + outInnerType.String() + ", only struct supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureOutCapacity(out *reflect.Value, csvLen int) error {
|
||||||
|
switch out.Kind() {
|
||||||
|
case reflect.Array:
|
||||||
|
if out.Len() < csvLen-1 { // Array is not big enough to hold the CSV content (arrays are not addressable)
|
||||||
|
return fmt.Errorf("array capacity problem: cannot store %d %s in %s", csvLen-1, out.Type().Elem().String(), out.Type().String())
|
||||||
|
}
|
||||||
|
case reflect.Slice:
|
||||||
|
if !out.CanAddr() && out.Len() < csvLen-1 { // Slice is not big enough tho hold the CSV content and is not addressable
|
||||||
|
return fmt.Errorf("slice capacity problem and is not addressable (did you forget &?)")
|
||||||
|
} else if out.CanAddr() && out.Len() < csvLen-1 {
|
||||||
|
out.Set(reflect.MakeSlice(out.Type(), csvLen-1, csvLen-1)) // Slice is not big enough, so grows it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCSVFieldPosition(key string, structInfo *structInfo, curHeaderCount int) *fieldInfo {
|
||||||
|
matchedFieldCount := 0
|
||||||
|
for _, field := range structInfo.Fields {
|
||||||
|
if field.matchesKey(key) {
|
||||||
|
if matchedFieldCount >= curHeaderCount {
|
||||||
|
return &field
|
||||||
|
}
|
||||||
|
matchedFieldCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNewOutInner(outInnerWasPointer bool, outInnerType reflect.Type) reflect.Value {
|
||||||
|
if outInnerWasPointer {
|
||||||
|
return reflect.New(outInnerType)
|
||||||
|
}
|
||||||
|
return reflect.New(outInnerType).Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setInnerField(outInner *reflect.Value, outInnerWasPointer bool, index []int, value string, omitEmpty bool) error {
|
||||||
|
oi := *outInner
|
||||||
|
if outInnerWasPointer {
|
||||||
|
// initialize nil pointer
|
||||||
|
if oi.IsNil() {
|
||||||
|
setField(oi, "", omitEmpty)
|
||||||
|
}
|
||||||
|
oi = outInner.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if oi.Kind() == reflect.Slice || oi.Kind() == reflect.Array {
|
||||||
|
i := index[0]
|
||||||
|
|
||||||
|
// grow slice when needed
|
||||||
|
if i >= oi.Cap() {
|
||||||
|
newcap := oi.Cap() + oi.Cap()/2
|
||||||
|
if newcap < 4 {
|
||||||
|
newcap = 4
|
||||||
|
}
|
||||||
|
newoi := reflect.MakeSlice(oi.Type(), oi.Len(), newcap)
|
||||||
|
reflect.Copy(newoi, oi)
|
||||||
|
oi.Set(newoi)
|
||||||
|
}
|
||||||
|
if i >= oi.Len() {
|
||||||
|
oi.SetLen(i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
item := oi.Index(i)
|
||||||
|
if len(index) > 1 {
|
||||||
|
return setInnerField(&item, false, index[1:], value, omitEmpty)
|
||||||
|
}
|
||||||
|
return setField(item, value, omitEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// because pointers can be nil need to recurse one index at a time and perform nil check
|
||||||
|
if len(index) > 1 {
|
||||||
|
nextField := oi.Field(index[0])
|
||||||
|
return setInnerField(&nextField, nextField.Kind() == reflect.Ptr, index[1:], value, omitEmpty)
|
||||||
|
}
|
||||||
|
return setField(oi.FieldByIndex(index), value, omitEmpty)
|
||||||
|
}
|
||||||
169
vendor/github.com/gocarina/gocsv/encode.go
generated
vendored
Normal file
169
vendor/github.com/gocarina/gocsv/encode.go
generated
vendored
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package gocsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrChannelIsClosed = errors.New("channel is closed")
|
||||||
|
)
|
||||||
|
|
||||||
|
type encoder struct {
|
||||||
|
out io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEncoder(out io.Writer) *encoder {
|
||||||
|
return &encoder{out}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFromChan(writer CSVWriter, c <-chan interface{}, omitHeaders bool) error {
|
||||||
|
// Get the first value. It wil determine the header structure.
|
||||||
|
firstValue, ok := <-c
|
||||||
|
if !ok {
|
||||||
|
return ErrChannelIsClosed
|
||||||
|
}
|
||||||
|
inValue, inType := getConcreteReflectValueAndType(firstValue) // Get the concrete type
|
||||||
|
if err := ensureStructOrPtr(inType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inInnerWasPointer := inType.Kind() == reflect.Ptr
|
||||||
|
inInnerStructInfo := getStructInfo(inType) // Get the inner struct info to get CSV annotations
|
||||||
|
csvHeadersLabels := make([]string, len(inInnerStructInfo.Fields))
|
||||||
|
for i, fieldInfo := range inInnerStructInfo.Fields { // Used to write the header (first line) in CSV
|
||||||
|
csvHeadersLabels[i] = fieldInfo.getFirstKey()
|
||||||
|
}
|
||||||
|
if !omitHeaders {
|
||||||
|
if err := writer.Write(csvHeadersLabels); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write := func(val reflect.Value) error {
|
||||||
|
for j, fieldInfo := range inInnerStructInfo.Fields {
|
||||||
|
csvHeadersLabels[j] = ""
|
||||||
|
inInnerFieldValue, err := getInnerField(val, inInnerWasPointer, fieldInfo.IndexChain) // Get the correct field header <-> position
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
csvHeadersLabels[j] = inInnerFieldValue
|
||||||
|
}
|
||||||
|
if err := writer.Write(csvHeadersLabels); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := write(inValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for v := range c {
|
||||||
|
val, _ := getConcreteReflectValueAndType(v) // Get the concrete type (not pointer) (Slice<?> or Array<?>)
|
||||||
|
if err := ensureStructOrPtr(inType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := write(val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.Flush()
|
||||||
|
return writer.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTo(writer CSVWriter, in interface{}, omitHeaders bool) error {
|
||||||
|
inValue, inType := getConcreteReflectValueAndType(in) // Get the concrete type (not pointer) (Slice<?> or Array<?>)
|
||||||
|
if err := ensureInType(inType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inInnerWasPointer, inInnerType := getConcreteContainerInnerType(inType) // Get the concrete inner type (not pointer) (Container<"?">)
|
||||||
|
if err := ensureInInnerType(inInnerType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inInnerStructInfo := getStructInfo(inInnerType) // Get the inner struct info to get CSV annotations
|
||||||
|
csvHeadersLabels := make([]string, len(inInnerStructInfo.Fields))
|
||||||
|
for i, fieldInfo := range inInnerStructInfo.Fields { // Used to write the header (first line) in CSV
|
||||||
|
csvHeadersLabels[i] = fieldInfo.getFirstKey()
|
||||||
|
}
|
||||||
|
if !omitHeaders {
|
||||||
|
if err := writer.Write(csvHeadersLabels); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inLen := inValue.Len()
|
||||||
|
for i := 0; i < inLen; i++ { // Iterate over container rows
|
||||||
|
for j, fieldInfo := range inInnerStructInfo.Fields {
|
||||||
|
csvHeadersLabels[j] = ""
|
||||||
|
inInnerFieldValue, err := getInnerField(inValue.Index(i), inInnerWasPointer, fieldInfo.IndexChain) // Get the correct field header <-> position
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
csvHeadersLabels[j] = inInnerFieldValue
|
||||||
|
}
|
||||||
|
if err := writer.Write(csvHeadersLabels); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.Flush()
|
||||||
|
return writer.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureStructOrPtr(t reflect.Type) error {
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Ptr:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot use " + t.String() + ", only slice or array supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the inType is an array or a slice
|
||||||
|
func ensureInType(outType reflect.Type) error {
|
||||||
|
switch outType.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Array:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot use " + outType.String() + ", only slice or array supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the inInnerType is of type struct
|
||||||
|
func ensureInInnerType(outInnerType reflect.Type) error {
|
||||||
|
switch outInnerType.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot use " + outInnerType.String() + ", only struct supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInnerField(outInner reflect.Value, outInnerWasPointer bool, index []int) (string, error) {
|
||||||
|
oi := outInner
|
||||||
|
if outInnerWasPointer {
|
||||||
|
if oi.IsNil() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
oi = outInner.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if oi.Kind() == reflect.Slice || oi.Kind() == reflect.Array {
|
||||||
|
i := index[0]
|
||||||
|
|
||||||
|
if i >= oi.Len() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
item := oi.Index(i)
|
||||||
|
if len(index) > 1 {
|
||||||
|
return getInnerField(item, false, index[1:])
|
||||||
|
}
|
||||||
|
return getFieldAsString(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// because pointers can be nil need to recurse one index at a time and perform nil check
|
||||||
|
if len(index) > 1 {
|
||||||
|
nextField := oi.Field(index[0])
|
||||||
|
return getInnerField(nextField, nextField.Kind() == reflect.Ptr, index[1:])
|
||||||
|
}
|
||||||
|
return getFieldAsString(oi.FieldByIndex(index))
|
||||||
|
}
|
||||||
242
vendor/github.com/gocarina/gocsv/reflect.go
generated
vendored
Normal file
242
vendor/github.com/gocarina/gocsv/reflect.go
generated
vendored
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
package gocsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Reflection helpers
|
||||||
|
|
||||||
|
type structInfo struct {
|
||||||
|
Fields []fieldInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldInfo is a struct field that should be mapped to a CSV column, or vice-versa
|
||||||
|
// Each IndexChain element before the last is the index of an the embedded struct field
|
||||||
|
// that defines Key as a tag
|
||||||
|
type fieldInfo struct {
|
||||||
|
keys []string
|
||||||
|
omitEmpty bool
|
||||||
|
IndexChain []int
|
||||||
|
defaultValue string
|
||||||
|
partial bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fieldInfo) getFirstKey() string {
|
||||||
|
return f.keys[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fieldInfo) matchesKey(key string) bool {
|
||||||
|
for _, k := range f.keys {
|
||||||
|
if key == k || strings.TrimSpace(key) == k || (f.partial && strings.Contains(key, k)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var structInfoCache sync.Map
|
||||||
|
var structMap = make(map[reflect.Type]*structInfo)
|
||||||
|
var structMapMutex sync.RWMutex
|
||||||
|
|
||||||
|
func getStructInfo(rType reflect.Type) *structInfo {
|
||||||
|
stInfo, ok := structInfoCache.Load(rType)
|
||||||
|
if ok {
|
||||||
|
return stInfo.(*structInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldsList := getFieldInfos(rType, []int{}, []string{})
|
||||||
|
stInfo = &structInfo{fieldsList}
|
||||||
|
structInfoCache.Store(rType, stInfo)
|
||||||
|
|
||||||
|
return stInfo.(*structInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFieldInfos(rType reflect.Type, parentIndexChain []int, parentKeys []string) []fieldInfo {
|
||||||
|
fieldsCount := rType.NumField()
|
||||||
|
fieldsList := make([]fieldInfo, 0, fieldsCount)
|
||||||
|
for i := 0; i < fieldsCount; i++ {
|
||||||
|
field := rType.Field(i)
|
||||||
|
if field.PkgPath != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var cpy = make([]int, len(parentIndexChain))
|
||||||
|
copy(cpy, parentIndexChain)
|
||||||
|
indexChain := append(cpy, i)
|
||||||
|
|
||||||
|
var currFieldInfo *fieldInfo
|
||||||
|
if !field.Anonymous {
|
||||||
|
filteredTags := []string{}
|
||||||
|
currFieldInfo, filteredTags = filterTags(TagName, indexChain, field)
|
||||||
|
|
||||||
|
if len(filteredTags) == 1 && filteredTags[0] == "-" {
|
||||||
|
// ignore nested structs with - tag
|
||||||
|
continue
|
||||||
|
} else if len(filteredTags) > 0 && filteredTags[0] != "" {
|
||||||
|
currFieldInfo.keys = filteredTags
|
||||||
|
} else {
|
||||||
|
currFieldInfo.keys = []string{normalizeName(field.Name)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parentKeys) > 0 && currFieldInfo != nil && !canMarshal(field.Type) {
|
||||||
|
// create cartesian product of keys
|
||||||
|
// eg: parent keys x field keys
|
||||||
|
keys := make([]string, 0, len(parentKeys)*len(currFieldInfo.keys))
|
||||||
|
for _, pkey := range parentKeys {
|
||||||
|
for _, ckey := range currFieldInfo.keys {
|
||||||
|
keys = append(keys, normalizeName(fmt.Sprintf("%s.%s", pkey, ckey)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currFieldInfo.keys = keys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle struct
|
||||||
|
fieldType := field.Type
|
||||||
|
// if the field is a pointer, follow the pointer
|
||||||
|
if fieldType.Kind() == reflect.Ptr {
|
||||||
|
fieldType = fieldType.Elem()
|
||||||
|
}
|
||||||
|
// if the field is a struct, create a fieldInfo for each of its fields
|
||||||
|
if fieldType.Kind() == reflect.Struct {
|
||||||
|
// Structs that implement any of the text or CSV marshaling methods
|
||||||
|
// should result in one value and not have their fields exposed
|
||||||
|
if !(canMarshal(fieldType)) {
|
||||||
|
// if the field is an embedded struct, pass along parent keys
|
||||||
|
keys := parentKeys
|
||||||
|
if currFieldInfo != nil {
|
||||||
|
keys = currFieldInfo.keys
|
||||||
|
}
|
||||||
|
fieldsList = append(fieldsList, getFieldInfos(fieldType, indexChain, keys)...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the field is an embedded struct, ignore the csv tag
|
||||||
|
if currFieldInfo == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array {
|
||||||
|
var arrayLength = -1
|
||||||
|
if arrayTag, ok := field.Tag.Lookup(TagName + "[]"); ok {
|
||||||
|
arrayLength, _ = strconv.Atoi(arrayTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the field is a slice/array of structs, create a fieldInfo for each index and each field
|
||||||
|
if field.Type.Elem().Kind() == reflect.Struct {
|
||||||
|
fieldInfos := getFieldInfos(field.Type.Elem(), []int{}, []string{})
|
||||||
|
|
||||||
|
for idx := 0; idx < arrayLength; idx++ {
|
||||||
|
// copy index chain and append array index
|
||||||
|
var cpy2 = make([]int, len(indexChain))
|
||||||
|
copy(cpy2, indexChain)
|
||||||
|
arrayIndexChain := append(cpy2, idx)
|
||||||
|
for _, childFieldInfo := range fieldInfos {
|
||||||
|
// copy array index chain and append array index
|
||||||
|
var cpy3 = make([]int, len(arrayIndexChain))
|
||||||
|
copy(cpy3, arrayIndexChain)
|
||||||
|
|
||||||
|
arrayFieldInfo := fieldInfo{
|
||||||
|
IndexChain: append(cpy3, childFieldInfo.IndexChain...),
|
||||||
|
omitEmpty: childFieldInfo.omitEmpty,
|
||||||
|
defaultValue: childFieldInfo.defaultValue,
|
||||||
|
partial: childFieldInfo.partial,
|
||||||
|
}
|
||||||
|
|
||||||
|
// create cartesian product of keys
|
||||||
|
// eg: array field keys x struct field keys
|
||||||
|
for _, akey := range currFieldInfo.keys {
|
||||||
|
for _, fkey := range childFieldInfo.keys {
|
||||||
|
arrayFieldInfo.keys = append(arrayFieldInfo.keys, normalizeName(fmt.Sprintf("%s[%d].%s", akey, idx, fkey)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldsList = append(fieldsList, arrayFieldInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if arrayLength > 0 {
|
||||||
|
// When the field is a slice/array of primitives, create a fieldInfo for each index
|
||||||
|
for idx := 0; idx < arrayLength; idx++ {
|
||||||
|
// copy index chain and append array index
|
||||||
|
var cpy2 = make([]int, len(indexChain))
|
||||||
|
copy(cpy2, indexChain)
|
||||||
|
|
||||||
|
arrayFieldInfo := fieldInfo{
|
||||||
|
IndexChain: append(cpy2, idx),
|
||||||
|
omitEmpty: currFieldInfo.omitEmpty,
|
||||||
|
defaultValue: currFieldInfo.defaultValue,
|
||||||
|
partial: currFieldInfo.partial,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, akey := range currFieldInfo.keys {
|
||||||
|
arrayFieldInfo.keys = append(arrayFieldInfo.keys, normalizeName(fmt.Sprintf("%s[%d]", akey, idx)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldsList = append(fieldsList, arrayFieldInfo)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fieldsList = append(fieldsList, *currFieldInfo)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fieldsList = append(fieldsList, *currFieldInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fieldsList
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterTags(tagName string, indexChain []int, field reflect.StructField) (*fieldInfo, []string) {
|
||||||
|
currFieldInfo := fieldInfo{IndexChain: indexChain}
|
||||||
|
|
||||||
|
fieldTag := field.Tag.Get(tagName)
|
||||||
|
fieldTags := strings.Split(fieldTag, TagSeparator)
|
||||||
|
|
||||||
|
filteredTags := []string{}
|
||||||
|
for _, fieldTagEntry := range fieldTags {
|
||||||
|
trimmedFieldTagEntry := strings.TrimSpace(fieldTagEntry) // handles cases like `csv:"foo, omitempty, default=test"`
|
||||||
|
if trimmedFieldTagEntry == "omitempty" {
|
||||||
|
currFieldInfo.omitEmpty = true
|
||||||
|
} else if strings.HasPrefix(trimmedFieldTagEntry, "partial") {
|
||||||
|
currFieldInfo.partial = true
|
||||||
|
} else if strings.HasPrefix(trimmedFieldTagEntry, "default=") {
|
||||||
|
currFieldInfo.defaultValue = strings.TrimPrefix(trimmedFieldTagEntry, "default=")
|
||||||
|
} else {
|
||||||
|
filteredTags = append(filteredTags, normalizeName(trimmedFieldTagEntry))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &currFieldInfo, filteredTags
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConcreteContainerInnerType(in reflect.Type) (inInnerWasPointer bool, inInnerType reflect.Type) {
|
||||||
|
inInnerType = in.Elem()
|
||||||
|
inInnerWasPointer = false
|
||||||
|
if inInnerType.Kind() == reflect.Ptr {
|
||||||
|
inInnerWasPointer = true
|
||||||
|
inInnerType = inInnerType.Elem()
|
||||||
|
}
|
||||||
|
return inInnerWasPointer, inInnerType
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConcreteReflectValueAndType(in interface{}) (reflect.Value, reflect.Type) {
|
||||||
|
value := reflect.ValueOf(in)
|
||||||
|
if value.Kind() == reflect.Ptr {
|
||||||
|
value = value.Elem()
|
||||||
|
}
|
||||||
|
return value, value.Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorInterface = reflect.TypeOf((*error)(nil)).Elem()
|
||||||
|
|
||||||
|
func isErrorType(outType reflect.Type) bool {
|
||||||
|
if outType.Kind() != reflect.Interface {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return outType.Implements(errorInterface)
|
||||||
|
}
|
||||||
38
vendor/github.com/gocarina/gocsv/safe_csv.go
generated
vendored
Normal file
38
vendor/github.com/gocarina/gocsv/safe_csv.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package gocsv
|
||||||
|
|
||||||
|
//Wraps around SafeCSVWriter and makes it thread safe.
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CSVWriter interface {
|
||||||
|
Write(row []string) error
|
||||||
|
Flush()
|
||||||
|
Error() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SafeCSVWriter struct {
|
||||||
|
*csv.Writer
|
||||||
|
m sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSafeCSVWriter(original *csv.Writer) *SafeCSVWriter {
|
||||||
|
return &SafeCSVWriter{
|
||||||
|
Writer: original,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Override write
|
||||||
|
func (w *SafeCSVWriter) Write(row []string) error {
|
||||||
|
w.m.Lock()
|
||||||
|
defer w.m.Unlock()
|
||||||
|
return w.Writer.Write(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Override flush
|
||||||
|
func (w *SafeCSVWriter) Flush() {
|
||||||
|
w.m.Lock()
|
||||||
|
w.Writer.Flush()
|
||||||
|
w.m.Unlock()
|
||||||
|
}
|
||||||
489
vendor/github.com/gocarina/gocsv/types.go
generated
vendored
Normal file
489
vendor/github.com/gocarina/gocsv/types.go
generated
vendored
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
package gocsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Conversion interfaces
|
||||||
|
|
||||||
|
var (
|
||||||
|
marshalerType = reflect.TypeOf(new(TypeMarshaller)).Elem()
|
||||||
|
textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem()
|
||||||
|
unmarshalerType = reflect.TypeOf(new(TypeUnmarshaller)).Elem()
|
||||||
|
unmarshalCSVWithFieldsType = reflect.TypeOf(new(TypeUnmarshalCSVWithFields)).Elem()
|
||||||
|
)
|
||||||
|
|
||||||
|
// TypeMarshaller is implemented by any value that has a MarshalCSV method
|
||||||
|
// This converter is used to convert the value to it string representation
|
||||||
|
type TypeMarshaller interface {
|
||||||
|
MarshalCSV() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeUnmarshaller is implemented by any value that has an UnmarshalCSV method
|
||||||
|
// This converter is used to convert a string to your value representation of that string
|
||||||
|
type TypeUnmarshaller interface {
|
||||||
|
UnmarshalCSV(string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeUnmarshalCSVWithFields can be implemented on whole structs to allow for whole structures to customized internal vs one off fields
|
||||||
|
type TypeUnmarshalCSVWithFields interface {
|
||||||
|
UnmarshalCSVWithFields(key, value string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoUnmarshalFuncError is the custom error type to be raised in case there is no unmarshal function defined on type
|
||||||
|
type NoUnmarshalFuncError struct {
|
||||||
|
t reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e NoUnmarshalFuncError) Error() string {
|
||||||
|
return "No known conversion from string to " + e.t.Name() + ", it does not implement TypeUnmarshaller"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoMarshalFuncError is the custom error type to be raised in case there is no marshal function defined on type
|
||||||
|
type NoMarshalFuncError struct {
|
||||||
|
ty reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e NoMarshalFuncError) Error() string {
|
||||||
|
return "No known conversion from " + e.ty.String() + " to string, " + e.ty.String() + " does not implement TypeMarshaller nor Stringer"
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Conversion helpers
|
||||||
|
|
||||||
|
func toString(in interface{}) (string, error) {
|
||||||
|
inValue := reflect.ValueOf(in)
|
||||||
|
|
||||||
|
switch inValue.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return inValue.String(), nil
|
||||||
|
case reflect.Bool:
|
||||||
|
b := inValue.Bool()
|
||||||
|
if b {
|
||||||
|
return "true", nil
|
||||||
|
}
|
||||||
|
return "false", nil
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return fmt.Sprintf("%v", inValue.Int()), nil
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return fmt.Sprintf("%v", inValue.Uint()), nil
|
||||||
|
case reflect.Float32:
|
||||||
|
return strconv.FormatFloat(inValue.Float(), byte('f'), -1, 32), nil
|
||||||
|
case reflect.Float64:
|
||||||
|
return strconv.FormatFloat(inValue.Float(), byte('f'), -1, 64), nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("No known conversion from " + inValue.Type().String() + " to string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBool(in interface{}) (bool, error) {
|
||||||
|
inValue := reflect.ValueOf(in)
|
||||||
|
|
||||||
|
switch inValue.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
s := inValue.String()
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if strings.EqualFold(s, "yes") {
|
||||||
|
return true, nil
|
||||||
|
} else if strings.EqualFold(s, "no") || s == "" {
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
return strconv.ParseBool(s)
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
return inValue.Bool(), nil
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
i := inValue.Int()
|
||||||
|
if i != 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
i := inValue.Uint()
|
||||||
|
if i != 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
f := inValue.Float()
|
||||||
|
if f != 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("No known conversion from " + inValue.Type().String() + " to bool")
|
||||||
|
}
|
||||||
|
|
||||||
|
func toInt(in interface{}) (int64, error) {
|
||||||
|
inValue := reflect.ValueOf(in)
|
||||||
|
|
||||||
|
switch inValue.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
s := strings.TrimSpace(inValue.String())
|
||||||
|
if s == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
out := strings.SplitN(s, ".", 2)
|
||||||
|
return strconv.ParseInt(out[0], 0, 64)
|
||||||
|
case reflect.Bool:
|
||||||
|
if inValue.Bool() {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return inValue.Int(), nil
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return int64(inValue.Uint()), nil
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return int64(inValue.Float()), nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("No known conversion from " + inValue.Type().String() + " to int")
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUint(in interface{}) (uint64, error) {
|
||||||
|
inValue := reflect.ValueOf(in)
|
||||||
|
|
||||||
|
switch inValue.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
s := strings.TrimSpace(inValue.String())
|
||||||
|
if s == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// support the float input
|
||||||
|
if strings.Contains(s, ".") {
|
||||||
|
f, err := strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return uint64(f), nil
|
||||||
|
}
|
||||||
|
return strconv.ParseUint(s, 0, 64)
|
||||||
|
case reflect.Bool:
|
||||||
|
if inValue.Bool() {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return uint64(inValue.Int()), nil
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return inValue.Uint(), nil
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return uint64(inValue.Float()), nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("No known conversion from " + inValue.Type().String() + " to uint")
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFloat(in interface{}) (float64, error) {
|
||||||
|
inValue := reflect.ValueOf(in)
|
||||||
|
|
||||||
|
switch inValue.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
s := strings.TrimSpace(inValue.String())
|
||||||
|
if s == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
s = strings.Replace(s, ",", ".", -1)
|
||||||
|
return strconv.ParseFloat(s, 64)
|
||||||
|
case reflect.Bool:
|
||||||
|
if inValue.Bool() {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return float64(inValue.Int()), nil
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return float64(inValue.Uint()), nil
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return inValue.Float(), nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("No known conversion from " + inValue.Type().String() + " to float")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setField(field reflect.Value, value string, omitEmpty bool) error {
|
||||||
|
if field.Kind() == reflect.Ptr {
|
||||||
|
if omitEmpty && value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if field.IsNil() {
|
||||||
|
field.Set(reflect.New(field.Type().Elem()))
|
||||||
|
}
|
||||||
|
field = field.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch field.Interface().(type) {
|
||||||
|
case string:
|
||||||
|
s, err := toString(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetString(s)
|
||||||
|
case bool:
|
||||||
|
b, err := toBool(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetBool(b)
|
||||||
|
case int, int8, int16, int32, int64:
|
||||||
|
i, err := toInt(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetInt(i)
|
||||||
|
case uint, uint8, uint16, uint32, uint64:
|
||||||
|
ui, err := toUint(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetUint(ui)
|
||||||
|
case float32, float64:
|
||||||
|
f, err := toFloat(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetFloat(f)
|
||||||
|
default:
|
||||||
|
// Not a native type, check for unmarshal method
|
||||||
|
if err := unmarshall(field, value); err != nil {
|
||||||
|
if _, ok := err.(NoUnmarshalFuncError); !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Could not unmarshal, check for kind, e.g. renamed type from basic type
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
s, err := toString(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetString(s)
|
||||||
|
case reflect.Bool:
|
||||||
|
b, err := toBool(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetBool(b)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
i, err := toInt(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetInt(i)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
ui, err := toUint(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetUint(ui)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
f, err := toFloat(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetFloat(f)
|
||||||
|
case reflect.Slice, reflect.Struct:
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(value), field.Addr().Interface())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFieldAsString(field reflect.Value) (str string, err error) {
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.Interface, reflect.Ptr:
|
||||||
|
if field.IsNil() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return getFieldAsString(field.Elem())
|
||||||
|
default:
|
||||||
|
// Check if field is go native type
|
||||||
|
switch field.Interface().(type) {
|
||||||
|
case string:
|
||||||
|
return field.String(), nil
|
||||||
|
case bool:
|
||||||
|
if field.Bool() {
|
||||||
|
return "true", nil
|
||||||
|
} else {
|
||||||
|
return "false", nil
|
||||||
|
}
|
||||||
|
case int, int8, int16, int32, int64:
|
||||||
|
return fmt.Sprintf("%v", field.Int()), nil
|
||||||
|
case uint, uint8, uint16, uint32, uint64:
|
||||||
|
return fmt.Sprintf("%v", field.Uint()), nil
|
||||||
|
case float32:
|
||||||
|
str, err = toString(float32(field.Float()))
|
||||||
|
if err != nil {
|
||||||
|
return str, err
|
||||||
|
}
|
||||||
|
case float64:
|
||||||
|
str, err = toString(field.Float())
|
||||||
|
if err != nil {
|
||||||
|
return str, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Not a native type, check for marshal method
|
||||||
|
str, err = marshall(field)
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(NoMarshalFuncError); !ok {
|
||||||
|
return str, err
|
||||||
|
}
|
||||||
|
// If not marshal method, is field compatible with/renamed from native type
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return field.String(), nil
|
||||||
|
case reflect.Bool:
|
||||||
|
str, err = toString(field.Bool())
|
||||||
|
if err != nil {
|
||||||
|
return str, err
|
||||||
|
}
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
str, err = toString(field.Int())
|
||||||
|
if err != nil {
|
||||||
|
return str, err
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
str, err = toString(field.Uint())
|
||||||
|
if err != nil {
|
||||||
|
return str, err
|
||||||
|
}
|
||||||
|
case reflect.Float32:
|
||||||
|
str, err = toString(float32(field.Float()))
|
||||||
|
if err != nil {
|
||||||
|
return str, err
|
||||||
|
}
|
||||||
|
case reflect.Float64:
|
||||||
|
str, err = toString(field.Float())
|
||||||
|
if err != nil {
|
||||||
|
return str, err
|
||||||
|
}
|
||||||
|
case reflect.Slice:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Array:
|
||||||
|
b, err := json.Marshal(field.Addr().Interface())
|
||||||
|
if err != nil {
|
||||||
|
return str, err
|
||||||
|
}
|
||||||
|
|
||||||
|
str = string(b)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return str, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Un/serializations helpers
|
||||||
|
|
||||||
|
func canMarshal(t reflect.Type) bool {
|
||||||
|
// Struct that implements any of the text or CSV marshaling interfaces
|
||||||
|
if t.Implements(marshalerType) ||
|
||||||
|
t.Implements(textMarshalerType) ||
|
||||||
|
t.Implements(unmarshalerType) ||
|
||||||
|
t.Implements(unmarshalCSVWithFieldsType) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointer to a struct that implements any of the text or CSV marshaling interfaces
|
||||||
|
t = reflect.PtrTo(t)
|
||||||
|
if t.Implements(marshalerType) ||
|
||||||
|
t.Implements(textMarshalerType) ||
|
||||||
|
t.Implements(unmarshalerType) ||
|
||||||
|
t.Implements(unmarshalCSVWithFieldsType) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshall(field reflect.Value, value string) error {
|
||||||
|
dupField := field
|
||||||
|
unMarshallIt := func(finalField reflect.Value) error {
|
||||||
|
if finalField.CanInterface() {
|
||||||
|
fieldIface := finalField.Interface()
|
||||||
|
|
||||||
|
fieldTypeUnmarshaller, ok := fieldIface.(TypeUnmarshaller)
|
||||||
|
if ok {
|
||||||
|
return fieldTypeUnmarshaller.UnmarshalCSV(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise try to use TextUnmarshaler
|
||||||
|
fieldTextUnmarshaler, ok := fieldIface.(encoding.TextUnmarshaler)
|
||||||
|
if ok {
|
||||||
|
return fieldTextUnmarshaler.UnmarshalText([]byte(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NoUnmarshalFuncError{field.Type()}
|
||||||
|
}
|
||||||
|
for dupField.Kind() == reflect.Interface || dupField.Kind() == reflect.Ptr {
|
||||||
|
if dupField.IsNil() {
|
||||||
|
dupField = reflect.New(field.Type().Elem())
|
||||||
|
field.Set(dupField)
|
||||||
|
return unMarshallIt(dupField)
|
||||||
|
}
|
||||||
|
dupField = dupField.Elem()
|
||||||
|
}
|
||||||
|
if dupField.CanAddr() {
|
||||||
|
return unMarshallIt(dupField.Addr())
|
||||||
|
}
|
||||||
|
return NoUnmarshalFuncError{field.Type()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshall(field reflect.Value) (value string, err error) {
|
||||||
|
dupField := field
|
||||||
|
marshallIt := func(finalField reflect.Value) (string, error) {
|
||||||
|
if finalField.CanInterface() {
|
||||||
|
fieldIface := finalField.Interface()
|
||||||
|
|
||||||
|
// Use TypeMarshaller when possible
|
||||||
|
fieldTypeMarhaller, ok := fieldIface.(TypeMarshaller)
|
||||||
|
if ok {
|
||||||
|
return fieldTypeMarhaller.MarshalCSV()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise try to use TextMarshaller
|
||||||
|
fieldTextMarshaler, ok := fieldIface.(encoding.TextMarshaler)
|
||||||
|
if ok {
|
||||||
|
text, err := fieldTextMarshaler.MarshalText()
|
||||||
|
return string(text), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise try to use Stringer
|
||||||
|
fieldStringer, ok := fieldIface.(fmt.Stringer)
|
||||||
|
if ok {
|
||||||
|
return fieldStringer.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, NoMarshalFuncError{field.Type()}
|
||||||
|
}
|
||||||
|
for dupField.Kind() == reflect.Interface || dupField.Kind() == reflect.Ptr {
|
||||||
|
if dupField.IsNil() {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
dupField = dupField.Elem()
|
||||||
|
}
|
||||||
|
if dupField.CanAddr() {
|
||||||
|
dupField = dupField.Addr()
|
||||||
|
}
|
||||||
|
return marshallIt(dupField)
|
||||||
|
}
|
||||||
134
vendor/github.com/gocarina/gocsv/unmarshaller.go
generated
vendored
Normal file
134
vendor/github.com/gocarina/gocsv/unmarshaller.go
generated
vendored
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package gocsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unmarshaller is a CSV to struct unmarshaller.
|
||||||
|
type Unmarshaller struct {
|
||||||
|
reader *csv.Reader
|
||||||
|
Headers []string
|
||||||
|
fieldInfoMap []*fieldInfo
|
||||||
|
MismatchedHeaders []string
|
||||||
|
MismatchedStructFields []string
|
||||||
|
outType reflect.Type
|
||||||
|
out interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnmarshaller creates an unmarshaller from a csv.Reader and a struct.
|
||||||
|
func NewUnmarshaller(reader *csv.Reader, out interface{}) (*Unmarshaller, error) {
|
||||||
|
headers, err := reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
headers = normalizeHeaders(headers)
|
||||||
|
|
||||||
|
um := &Unmarshaller{reader: reader, outType: reflect.TypeOf(out)}
|
||||||
|
err = validate(um, out, headers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return um, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read returns an interface{} whose runtime type is the same as the struct that
|
||||||
|
// was used to create the Unmarshaller.
|
||||||
|
func (um *Unmarshaller) Read() (interface{}, error) {
|
||||||
|
row, err := um.reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return um.unmarshalRow(row, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadUnmatched is same as Read(), but returns a map of the columns that didn't match a field in the struct
|
||||||
|
func (um *Unmarshaller) ReadUnmatched() (interface{}, map[string]string, error) {
|
||||||
|
row, err := um.reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
unmatched := make(map[string]string)
|
||||||
|
value, err := um.unmarshalRow(row, unmatched)
|
||||||
|
return value, unmatched, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate ensures that a struct was used to create the Unmarshaller, and validates
|
||||||
|
// CSV headers against the CSV tags in the struct.
|
||||||
|
func validate(um *Unmarshaller, s interface{}, headers []string) error {
|
||||||
|
concreteType := reflect.TypeOf(s)
|
||||||
|
if concreteType.Kind() == reflect.Ptr {
|
||||||
|
concreteType = concreteType.Elem()
|
||||||
|
}
|
||||||
|
if err := ensureOutInnerType(concreteType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
structInfo := getStructInfo(concreteType) // Get struct info to get CSV annotations.
|
||||||
|
if len(structInfo.Fields) == 0 {
|
||||||
|
return ErrNoStructTags
|
||||||
|
}
|
||||||
|
csvHeadersLabels := make([]*fieldInfo, len(headers)) // Used to store the corresponding header <-> position in CSV
|
||||||
|
headerCount := map[string]int{}
|
||||||
|
for i, csvColumnHeader := range headers {
|
||||||
|
curHeaderCount := headerCount[csvColumnHeader]
|
||||||
|
if fieldInfo := getCSVFieldPosition(csvColumnHeader, structInfo, curHeaderCount); fieldInfo != nil {
|
||||||
|
csvHeadersLabels[i] = fieldInfo
|
||||||
|
if ShouldAlignDuplicateHeadersWithStructFieldOrder {
|
||||||
|
curHeaderCount++
|
||||||
|
headerCount[csvColumnHeader] = curHeaderCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if FailIfDoubleHeaderNames {
|
||||||
|
if err := maybeDoubleHeaderNames(headers); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
um.Headers = headers
|
||||||
|
um.fieldInfoMap = csvHeadersLabels
|
||||||
|
um.MismatchedHeaders = mismatchHeaderFields(structInfo.Fields, headers)
|
||||||
|
um.MismatchedStructFields = mismatchStructFields(structInfo.Fields, headers)
|
||||||
|
um.out = s
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalRow converts a CSV row to a struct, based on CSV struct tags.
|
||||||
|
// If unmatched is non nil, it is populated with any columns that don't map to a struct field
|
||||||
|
func (um *Unmarshaller) unmarshalRow(row []string, unmatched map[string]string) (interface{}, error) {
|
||||||
|
isPointer := false
|
||||||
|
concreteOutType := um.outType
|
||||||
|
if um.outType.Kind() == reflect.Ptr {
|
||||||
|
isPointer = true
|
||||||
|
concreteOutType = concreteOutType.Elem()
|
||||||
|
}
|
||||||
|
outValue := createNewOutInner(isPointer, concreteOutType)
|
||||||
|
for j, csvColumnContent := range row {
|
||||||
|
if j < len(um.fieldInfoMap) && um.fieldInfoMap[j] != nil {
|
||||||
|
fieldInfo := um.fieldInfoMap[j]
|
||||||
|
if err := setInnerField(&outValue, isPointer, fieldInfo.IndexChain, csvColumnContent, fieldInfo.omitEmpty); err != nil { // Set field of struct
|
||||||
|
return nil, fmt.Errorf("cannot assign field at %v to %s through index chain %v: %v", j, outValue.Type(), fieldInfo.IndexChain, err)
|
||||||
|
}
|
||||||
|
} else if unmatched != nil {
|
||||||
|
unmatched[um.Headers[j]] = csvColumnContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outValue.Interface(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenormalizeHeaders will remap the header names based on the headerNormalizer.
|
||||||
|
// This can be used to map a CSV to a struct where the CSV header names do not match in the file but a mapping is known
|
||||||
|
func (um *Unmarshaller) RenormalizeHeaders(headerNormalizer func([]string) []string) error {
|
||||||
|
headers := um.Headers
|
||||||
|
if headerNormalizer != nil {
|
||||||
|
headers = headerNormalizer(headers)
|
||||||
|
}
|
||||||
|
err := validate(um, um.out, headers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
6
vendor/modules.txt
vendored
6
vendor/modules.txt
vendored
@@ -39,6 +39,9 @@ github.com/fatih/color
|
|||||||
# github.com/fsnotify/fsnotify v1.6.0
|
# github.com/fsnotify/fsnotify v1.6.0
|
||||||
## explicit; go 1.16
|
## explicit; go 1.16
|
||||||
github.com/fsnotify/fsnotify
|
github.com/fsnotify/fsnotify
|
||||||
|
# github.com/gocarina/gocsv v0.0.0-20230123225133-763e25b40669
|
||||||
|
## explicit; go 1.13
|
||||||
|
github.com/gocarina/gocsv
|
||||||
# github.com/golang/mock v1.6.0
|
# github.com/golang/mock v1.6.0
|
||||||
## explicit; go 1.11
|
## explicit; go 1.11
|
||||||
github.com/golang/mock/gomock
|
github.com/golang/mock/gomock
|
||||||
@@ -204,6 +207,9 @@ golang.org/x/crypto/internal/alias
|
|||||||
golang.org/x/crypto/internal/poly1305
|
golang.org/x/crypto/internal/poly1305
|
||||||
golang.org/x/crypto/ssh
|
golang.org/x/crypto/ssh
|
||||||
golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
|
golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
|
||||||
|
# golang.org/x/exp v0.0.0-20230206171751-46f607a40771
|
||||||
|
## explicit; go 1.18
|
||||||
|
golang.org/x/exp/maps
|
||||||
# golang.org/x/net v0.5.0
|
# golang.org/x/net v0.5.0
|
||||||
## explicit; go 1.17
|
## explicit; go 1.17
|
||||||
golang.org/x/net/context
|
golang.org/x/net/context
|
||||||
|
|||||||
Reference in New Issue
Block a user