mirror of
https://github.com/LukeHagar/sailpoint-cli.git
synced 2025-12-06 04:21:15 +00:00
Adjusted Project Structure, added environment support, added 90% spconfig functionality
This commit is contained in:
@@ -2,234 +2,33 @@
|
||||
package configure
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"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/sailpoint-oss/sailpoint-cli/internal/config"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/terminal"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
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 {
|
||||
func NewConfigureCmd() *cobra.Command {
|
||||
var debug bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "configure",
|
||||
Short: "configure authentication for the cli",
|
||||
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})",
|
||||
Short: "configure pat authentication for the currently active environment",
|
||||
Long: "\nConfigure PAT Authentication for the CLI\n\nPrerequisites:\n\nClient ID\nClient Secret\n",
|
||||
Aliases: []string{"conf"},
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
viper.Set("debug", debug)
|
||||
|
||||
var AuthType string
|
||||
var err error
|
||||
ClientID := terminal.InputPrompt("Personal Access Token Client ID:")
|
||||
ClientSecret := terminal.InputPrompt("Personal Access Token Client Secret:")
|
||||
|
||||
if len(args) > 0 {
|
||||
AuthType = args[0]
|
||||
} 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")
|
||||
}
|
||||
config.SetPatClientID(ClientID)
|
||||
config.SetPatClientSecret(ClientSecret)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"gopkg.in/yaml.v2"
|
||||
@@ -17,7 +18,7 @@ const (
|
||||
connectorsEndpoint = "/beta/platform-connectors"
|
||||
)
|
||||
|
||||
func NewConnCmd(client client.Client) *cobra.Command {
|
||||
func NewConnCmd() *cobra.Command {
|
||||
conn := &cobra.Command{
|
||||
Use: "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.AddCommand(
|
||||
newConnInitCmd(),
|
||||
newConnListCmd(client),
|
||||
newConnGetCmd(client),
|
||||
newConnUpdateCmd(client),
|
||||
newConnCreateCmd(client),
|
||||
newConnCreateVersionCmd(client),
|
||||
newConnVersionsCmd(client),
|
||||
newConnInvokeCmd(client),
|
||||
newConnValidateCmd(client),
|
||||
newConnTagCmd(client),
|
||||
newConnValidateSourcesCmd(client),
|
||||
newConnLogsCmd(client),
|
||||
newConnStatsCmd(client),
|
||||
newConnDeleteCmd(client),
|
||||
newConnListCmd(Client),
|
||||
newConnGetCmd(Client),
|
||||
newConnUpdateCmd(Client),
|
||||
newConnCreateCmd(Client),
|
||||
newConnCreateVersionCmd(Client),
|
||||
newConnVersionsCmd(Client),
|
||||
newConnInvokeCmd(Client),
|
||||
newConnValidateCmd(Client),
|
||||
newConnTagCmd(Client),
|
||||
newConnValidateSourcesCmd(Client),
|
||||
newConnLogsCmd(Client),
|
||||
newConnStatsCmd(Client),
|
||||
newConnDeleteCmd(Client),
|
||||
)
|
||||
|
||||
return conn
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ func newConnInvokeChangePasswordCmd(spClient client.Client) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := util.PromptPassword("Enter Password:")
|
||||
password, err := terminal.PromptPassword("Enter Password:")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/mocks"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
||||
)
|
||||
|
||||
@@ -33,7 +32,7 @@ func TestNewConnCmd_noArgs(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
cmd := NewConnCmd(mocks.NewMockClient(ctrl))
|
||||
cmd := NewConnCmd()
|
||||
if len(cmd.Commands()) != numConnSubcommands {
|
||||
t.Fatalf("expected: %d, actual: %d", len(cmd.Commands()), numConnSubcommands)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
package root
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/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 {
|
||||
cmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
@@ -17,12 +32,14 @@ func newAuthCommand() *cobra.Command {
|
||||
Example: "sail auth pat | oauth",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var selection string
|
||||
var err error
|
||||
|
||||
if len(args) > 0 {
|
||||
selection = args[0]
|
||||
} else {
|
||||
selection, err = configure.PromptAuth()
|
||||
selection, err = PromptAuth()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -30,23 +47,17 @@ func newAuthCommand() *cobra.Command {
|
||||
|
||||
switch strings.ToLower(selection) {
|
||||
case "pat":
|
||||
viper.Set("authtype", "pat")
|
||||
case "oauth":
|
||||
viper.Set("authtype", "oauth")
|
||||
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
|
||||
if config.GetAuthType() != "pat" {
|
||||
config.SetAuthType("pat")
|
||||
color.Blue("authentication method set to pat")
|
||||
}
|
||||
case "oauth":
|
||||
if config.GetAuthType() != "oauth" {
|
||||
config.SetAuthType("oauth")
|
||||
color.Blue("authentication method set to oauth")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid selection")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -23,18 +23,6 @@ func newDebugCommand() *cobra.Command {
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,20 +4,19 @@ package root
|
||||
import (
|
||||
"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/connector"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/environment"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/search"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/spconfig"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/transform"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/cmd/va"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
||||
"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{
|
||||
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.",
|
||||
@@ -35,12 +34,13 @@ func NewRootCmd(client client.Client, apiClient *sailpoint.APIClient) *cobra.Com
|
||||
root.AddCommand(
|
||||
newDebugCommand(),
|
||||
newAuthCommand(),
|
||||
configure.NewConfigureCmd(client),
|
||||
connector.NewConnCmd(client),
|
||||
transform.NewTransformCmd(client),
|
||||
environment.NewEnvironmentCommand(),
|
||||
configure.NewConfigureCmd(),
|
||||
connector.NewConnCmd(),
|
||||
transform.NewTransformCmd(),
|
||||
va.NewVACmd(),
|
||||
search.NewSearchCmd(apiClient),
|
||||
spconfig.NewSPConfigCmd(apiClient),
|
||||
search.NewSearchCmd(),
|
||||
spconfig.NewSPConfigCmd(),
|
||||
)
|
||||
return root
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/mocks"
|
||||
)
|
||||
|
||||
// Expected number of subcommands to `sp` root command
|
||||
@@ -18,7 +17,7 @@ func TestNewRootCmd_noArgs(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
cmd := NewRootCmd(mocks.NewMockClient(ctrl), nil)
|
||||
cmd := NewRootCmd()
|
||||
if 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)
|
||||
defer ctrl.Finish()
|
||||
|
||||
cmd := NewRootCmd(mocks.NewMockClient(ctrl), nil)
|
||||
cmd := NewRootCmd()
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
cmd.SetOut(b)
|
||||
|
||||
@@ -5,14 +5,15 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/search"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newQueryCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
var output string
|
||||
func newQueryCmd() *cobra.Command {
|
||||
var folderPath string
|
||||
var indicies []string
|
||||
var outputTypes []string
|
||||
var sort []string
|
||||
var searchQuery string
|
||||
cmd := &cobra.Command{
|
||||
@@ -24,28 +25,28 @@ func newQueryCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if output == "" {
|
||||
output = "search_results"
|
||||
apiClient := config.InitAPIClient()
|
||||
|
||||
if folderPath == "" {
|
||||
folderPath = "search_results"
|
||||
}
|
||||
|
||||
searchQuery = args[0]
|
||||
fmt.Println(searchQuery)
|
||||
|
||||
search, err := util.BuildSearch(searchQuery, sort, indicies)
|
||||
searchObj, err := search.BuildSearch(searchQuery, sort, indicies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("query=%s&indicie=%s.json", searchQuery, indicies)
|
||||
|
||||
err = util.SaveResults(formattedResponse, fileName, output)
|
||||
err = search.IterateIndicies(formattedResponse, searchQuery, folderPath, outputTypes)
|
||||
if err != nil {
|
||||
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(&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")
|
||||
|
||||
|
||||
@@ -4,11 +4,10 @@ package search
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewSearchCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
func NewSearchCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "search",
|
||||
Short: "perform search in identitynow with a search string",
|
||||
@@ -22,8 +21,8 @@ func NewSearchCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
newQueryCmd(apiClient),
|
||||
newTemplateCmd(apiClient),
|
||||
newQueryCmd(),
|
||||
newTemplateCmd(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
||||
@@ -7,14 +7,17 @@ import (
|
||||
"strings"
|
||||
|
||||
"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/util"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newTemplateCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
var output string
|
||||
func newTemplateCmd() *cobra.Command {
|
||||
var outputTypes []string
|
||||
var folderPath string
|
||||
var template string
|
||||
cmd := &cobra.Command{
|
||||
Use: "template",
|
||||
@@ -25,12 +28,10 @@ func newTemplateCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if output == "" {
|
||||
output = "search_results"
|
||||
}
|
||||
apiClient := config.InitAPIClient()
|
||||
|
||||
var selectedTemplate types.SearchTemplate
|
||||
searchTemplates, err := util.GetSearchTemplates()
|
||||
var selectedTemplate templates.SearchTemplate
|
||||
searchTemplates, err := templates.GetSearchTemplates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -38,26 +39,7 @@ func newTemplateCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
if len(args) > 0 {
|
||||
template = args[0]
|
||||
} else {
|
||||
// var prompts []types.Choice
|
||||
// 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)
|
||||
template, err = templates.SelectTemplate(searchTemplates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -68,18 +50,18 @@ func newTemplateCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("no template matches for %s", template)
|
||||
} else if len(matches) > 1 {
|
||||
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)
|
||||
if varCount > 0 {
|
||||
for i := 0; i < varCount; 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))
|
||||
}
|
||||
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)
|
||||
|
||||
formattedResponse, err := util.PerformSearch(*apiClient, selectedTemplate.SearchQuery)
|
||||
formattedResponse, err := search.PerformSearch(*apiClient, selectedTemplate.SearchQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("query=%s&indicie=%s.json", selectedTemplate.SearchQuery.Query.GetQuery(), selectedTemplate.SearchQuery.Indices)
|
||||
|
||||
err = util.SaveResults(formattedResponse, fileName, output)
|
||||
err = search.IterateIndicies(formattedResponse, selectedTemplate.SearchQuery.Query.GetQuery(), folderPath, outputTypes)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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"
|
||||
"fmt"
|
||||
|
||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
||||
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"
|
||||
)
|
||||
|
||||
func newExportCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
func newExportCmd() *cobra.Command {
|
||||
var folderPath string
|
||||
var description string
|
||||
var includeTypes []string
|
||||
var excludeTypes []string
|
||||
var exportAll bool
|
||||
var wait bool
|
||||
var payload *sailpointbetasdk.ExportPayload
|
||||
cmd := &cobra.Command{
|
||||
Use: "export",
|
||||
@@ -26,6 +27,8 @@ func newExportCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
apiClient := config.InitAPIClient()
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
payload = sailpointbetasdk.NewExportPayload()
|
||||
@@ -33,22 +36,27 @@ func newExportCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
payload.IncludeTypes = includeTypes
|
||||
payload.ExcludeTypes = excludeTypes
|
||||
|
||||
fmt.Println(payload.GetIncludeTypes())
|
||||
|
||||
job, _, err := apiClient.Beta.SPConfigApi.SpConfigExport(ctx).ExportPayload(*payload).Execute()
|
||||
if err != nil {
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
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().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(&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
|
||||
}
|
||||
|
||||
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 (
|
||||
"fmt"
|
||||
|
||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewSPConfigCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
func NewSPConfigCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "spconfig",
|
||||
Short: "perform spconfig operations in identitynow",
|
||||
@@ -22,8 +21,11 @@ func NewSPConfigCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
newExportCmd(apiClient),
|
||||
newExportStatusCmd(apiClient),
|
||||
newExportCmd(),
|
||||
newExportStatusCmd(),
|
||||
newTemplateCmd(),
|
||||
newDownloadCmd(),
|
||||
newImportCommand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
||||
@@ -4,12 +4,12 @@ package spconfig
|
||||
import (
|
||||
"context"
|
||||
|
||||
sailpoint "github.com/sailpoint-oss/golang-sdk/sdk-output"
|
||||
"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"
|
||||
)
|
||||
|
||||
func newExportStatusCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
func newExportStatusCmd() *cobra.Command {
|
||||
var exportJobs []string
|
||||
var importJobs []string
|
||||
cmd := &cobra.Command{
|
||||
@@ -21,6 +21,8 @@ func newExportStatusCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
apiClient := config.InitAPIClient()
|
||||
|
||||
for i := 0; i < len(exportJobs); i++ {
|
||||
job := exportJobs[i]
|
||||
ctx := context.TODO()
|
||||
@@ -29,7 +31,7 @@ func newExportStatusCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
util.PrintJob(*status)
|
||||
spconfig.PrintJob(*status)
|
||||
}
|
||||
|
||||
for i := 0; i < len(importJobs); i++ {
|
||||
@@ -40,7 +42,7 @@ func newExportStatusCmd(apiClient *sailpoint.APIClient) *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
util.PrintJob(*status)
|
||||
spconfig.PrintJob(*status)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
||||
sailpointsdk "github.com/sailpoint-oss/golang-sdk/sdk-output/v3"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newCreateCmd(client client.Client) *cobra.Command {
|
||||
func newCreateCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "create transform",
|
||||
@@ -49,26 +47,15 @@ func newCreateCmd(client client.Client) *cobra.Command {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint := cmd.Flags().Lookup("transforms-endpoint").Value.String()
|
||||
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)
|
||||
err = ListTransforms()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestNewCreateCmd(t *testing.T) {
|
||||
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := newCreateCmd(client)
|
||||
cmd := newCreateCmd()
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
cmd.SetOut(b)
|
||||
|
||||
@@ -2,23 +2,20 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"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"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newDeleteCmd(client client.Client) *cobra.Command {
|
||||
func newDeleteCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete [TRANSFORM-ID]",
|
||||
Short: "delete transform",
|
||||
@@ -28,17 +25,13 @@ func newDeleteCmd(client client.Client) *cobra.Command {
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
endpoint := cmd.Flags().Lookup("transforms-endpoint").Value.String()
|
||||
|
||||
id := ""
|
||||
var id string
|
||||
|
||||
if len(args) > 0 {
|
||||
id = args[0]
|
||||
}
|
||||
} else {
|
||||
|
||||
if id == "" {
|
||||
|
||||
transforms, err := getTransforms(client, endpoint, cmd)
|
||||
transforms, err := GetTransforms()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -52,7 +45,8 @@ func newDeleteCmd(client client.Client) *cobra.Command {
|
||||
var rows []table.Row
|
||||
|
||||
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(
|
||||
@@ -85,25 +79,18 @@ func newDeleteCmd(client client.Client) *cobra.Command {
|
||||
if len(tempRow) > 0 {
|
||||
id = m.Retrieve()[1]
|
||||
} 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 {
|
||||
return err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
_ = Body.Close()
|
||||
}(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("delete transform failed. status: %s\nbody: %s", resp.Status, body)
|
||||
}
|
||||
|
||||
err = listTransforms(client, endpoint, cmd)
|
||||
err = ListTransforms()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestNewDeleteCmd(t *testing.T) {
|
||||
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := newDeleteCmd(client)
|
||||
cmd := newDeleteCmd()
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
cmd.SetOut(b)
|
||||
|
||||
@@ -3,19 +3,15 @@ package transform
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newDownloadCmd(client client.Client) *cobra.Command {
|
||||
func newDownloadCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "download",
|
||||
Short: "download transforms",
|
||||
@@ -24,44 +20,21 @@ func newDownloadCmd(client client.Client) *cobra.Command {
|
||||
Aliases: []string{"dl"},
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
endpoint := cmd.Flags().Lookup("transforms-endpoint").Value.String()
|
||||
|
||||
resp, err := client.Get(cmd.Context(), endpoint)
|
||||
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)
|
||||
transforms, err := GetTransforms()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
destination := cmd.Flags().Lookup("destination").Value.String()
|
||||
|
||||
err = listTransforms(client, endpoint, cmd)
|
||||
err = ListTransforms()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range transforms {
|
||||
filename := strings.ReplaceAll(v["name"].(string), " ", "") + ".json"
|
||||
filename := strings.ReplaceAll(v.Name, " ", "") + ".json"
|
||||
content, _ := json.MarshalIndent(v, "", " ")
|
||||
|
||||
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).
|
||||
Times(1)
|
||||
|
||||
cmd := newListCmd(client)
|
||||
cmd := newListCmd()
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
cmd.SetOut(b)
|
||||
|
||||
@@ -2,63 +2,10 @@
|
||||
package transform
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func getTransforms(client client.Client, endpoint string, cmd *cobra.Command) ([]transmodel.Transform, error) {
|
||||
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 {
|
||||
func newListCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "list transforms",
|
||||
@@ -67,9 +14,8 @@ func newListCmd(client client.Client) *cobra.Command {
|
||||
Aliases: []string{"ls"},
|
||||
Args: cobra.NoArgs,
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestNewListCmd(t *testing.T) {
|
||||
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("[]")))}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := newListCmd(client)
|
||||
cmd := newListCmd()
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
cmd.SetOut(b)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
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/config"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
@@ -19,7 +20,7 @@ import (
|
||||
|
||||
var implicitInput bool
|
||||
|
||||
func newPreviewCmd(client client.Client) *cobra.Command {
|
||||
func newPreviewCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "preview",
|
||||
Short: "preview transform",
|
||||
@@ -31,6 +32,13 @@ func newPreviewCmd(client client.Client) *cobra.Command {
|
||||
idProfile := cmd.Flags().Lookup("identity-profile").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{}
|
||||
|
||||
if !implicitInput {
|
||||
@@ -58,7 +66,7 @@ func newPreviewCmd(client client.Client) *cobra.Command {
|
||||
// original transform for the attribute, which will contain the account attribute
|
||||
// name and source name that will be used in the preview body.
|
||||
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 {
|
||||
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+"\"}]")
|
||||
uri.RawQuery = query.Encode()
|
||||
|
||||
resp, err = client.Get(cmd.Context(), uri.String())
|
||||
resp, err = Client.Get(cmd.Context(), uri.String())
|
||||
if err != nil {
|
||||
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
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -15,7 +21,34 @@ const (
|
||||
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{
|
||||
Use: "transform",
|
||||
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.AddCommand(
|
||||
newListCmd(client),
|
||||
newDownloadCmd(client),
|
||||
newCreateCmd(client),
|
||||
newUpdateCmd(client),
|
||||
newDeleteCmd(client),
|
||||
newPreviewCmd(client),
|
||||
newListCmd(),
|
||||
newDownloadCmd(),
|
||||
newCreateCmd(),
|
||||
newUpdateCmd(),
|
||||
newDeleteCmd(),
|
||||
newPreviewCmd(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/client"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/util"
|
||||
sailpointsdk "github.com/sailpoint-oss/golang-sdk/sdk-output/v3"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newUpdateCmd(client client.Client) *cobra.Command {
|
||||
func newUpdateCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "update transform",
|
||||
@@ -51,25 +49,14 @@ func newUpdateCmd(client client.Client) *cobra.Command {
|
||||
id := data["id"].(string)
|
||||
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 {
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestNewUpdateCmd(t *testing.T) {
|
||||
Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil).
|
||||
Times(1)
|
||||
|
||||
cmd := newUpdateCmd(client)
|
||||
cmd := newUpdateCmd()
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
cmd.SetOut(b)
|
||||
|
||||
@@ -6,7 +6,8 @@ import (
|
||||
"path"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -36,7 +37,7 @@ func newCollectCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -45,7 +46,7 @@ func newCollectCmd() *cobra.Command {
|
||||
password := credentials[host]
|
||||
outputFolder := path.Join(output, endpoint)
|
||||
|
||||
err := util.CollectVAFiles(endpoint, password, outputFolder, files)
|
||||
err := va.CollectVAFiles(endpoint, password, outputFolder, files)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
"path"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -28,7 +29,7 @@ func newTroubleshootCmd() *cobra.Command {
|
||||
|
||||
var credentials []string
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -45,7 +46,7 @@ func newTroubleshootCmd() *cobra.Command {
|
||||
|
||||
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 {
|
||||
return orgErr
|
||||
}
|
||||
@@ -53,7 +54,7 @@ func newTroubleshootCmd() *cobra.Command {
|
||||
color.Green("Troubleshooting Complete")
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import (
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -18,20 +19,20 @@ func newUpdateCmd() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var credentials []string
|
||||
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)
|
||||
}
|
||||
for i := 0; i < len(args); i++ {
|
||||
endpoint := args[i]
|
||||
fmt.Printf("Starting update for %v\n", endpoint)
|
||||
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 {
|
||||
return updateErr
|
||||
} else {
|
||||
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 {
|
||||
color.Green("Rebooting Virtual Appliance (%v)", endpoint)
|
||||
} else {
|
||||
|
||||
2
go.mod
2
go.mod
@@ -7,6 +7,7 @@ require (
|
||||
github.com/charmbracelet/bubbletea v0.23.1
|
||||
github.com/charmbracelet/lipgloss v0.6.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/kr/pretty v0.3.1
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
@@ -20,6 +21,7 @@ require (
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/vbauerster/mpb/v8 v8.1.4
|
||||
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/term v0.4.0
|
||||
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/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/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/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=
|
||||
@@ -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-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-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-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
||||
@@ -10,8 +10,7 @@ import (
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
|
||||
clierrors "github.com/sailpoint-oss/sailpoint-cli/internal/errors"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/types"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -24,12 +23,12 @@ type Client interface {
|
||||
|
||||
// SpClient provides access to SP APIs.
|
||||
type SpClient struct {
|
||||
cfg types.CLIConfig
|
||||
cfg config.CLIConfig
|
||||
client *http.Client
|
||||
accessToken string
|
||||
}
|
||||
|
||||
func NewSpClient(cfg types.CLIConfig) Client {
|
||||
func NewSpClient(cfg config.CLIConfig) Client {
|
||||
return &SpClient{
|
||||
cfg: cfg,
|
||||
client: &http.Client{},
|
||||
@@ -41,10 +40,7 @@ func (c *SpClient) Get(ctx context.Context, url string) (*http.Response, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseUrl, err := c.cfg.GetBaseUrl()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseUrl := config.GetBaseUrl()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseUrl+url, nil)
|
||||
if err != nil {
|
||||
@@ -74,10 +70,7 @@ func (c *SpClient) Delete(ctx context.Context, url string, params map[string]str
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseUrl, err := c.cfg.GetBaseUrl()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseUrl := config.GetBaseUrl()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, baseUrl+url, nil)
|
||||
if err != nil {
|
||||
@@ -115,10 +108,7 @@ func (c *SpClient) Post(ctx context.Context, url string, contentType string, bod
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseUrl, err := c.cfg.GetBaseUrl()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseUrl := config.GetBaseUrl()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseUrl+url, body)
|
||||
if err != nil {
|
||||
@@ -149,10 +139,7 @@ func (c *SpClient) Put(ctx context.Context, url string, contentType string, body
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseUrl, err := c.cfg.GetBaseUrl()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseUrl := config.GetBaseUrl()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, baseUrl+url, body)
|
||||
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 {
|
||||
err := c.cfg.Validate()
|
||||
err := config.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -190,30 +177,32 @@ func (c *SpClient) ensureAccessToken(ctx context.Context) error {
|
||||
}
|
||||
|
||||
var cachedTokenExpiry time.Time
|
||||
switch c.cfg.GetAuthType() {
|
||||
switch config.GetAuthType() {
|
||||
case "pat":
|
||||
cachedTokenExpiry = viper.GetTime("pat.token.expiry")
|
||||
if cachedTokenExpiry.After(time.Now()) {
|
||||
c.accessToken = viper.GetString("pat.token.accesstoken")
|
||||
} else {
|
||||
return clierrors.ErrAccessTokenExpired
|
||||
err := config.PATLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case "oauth":
|
||||
cachedTokenExpiry = viper.GetTime("oauth.token.expiry")
|
||||
if cachedTokenExpiry.After(time.Now()) {
|
||||
c.accessToken = viper.GetString("oauth.token.accesstoken")
|
||||
} else {
|
||||
return clierrors.ErrAccessTokenExpired
|
||||
err := config.OAuthLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
return errors.New("invalid authtype configured")
|
||||
|
||||
}
|
||||
|
||||
if c.accessToken != "" {
|
||||
return fmt.Errorf("no token present")
|
||||
}
|
||||
|
||||
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"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/types"
|
||||
)
|
||||
|
||||
var docStyle = lipgloss.NewStyle().Margin(1, 2)
|
||||
@@ -16,6 +15,11 @@ type ListItem struct {
|
||||
description string
|
||||
}
|
||||
|
||||
type Choice struct {
|
||||
Title string
|
||||
Description string
|
||||
}
|
||||
|
||||
var choice ListItem
|
||||
|
||||
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{}
|
||||
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())
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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/internal/client"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/types"
|
||||
"github.com/sailpoint-oss/sailpoint-cli/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
c client.Client
|
||||
rootCmd *cobra.Command
|
||||
)
|
||||
var rootCmd *cobra.Command
|
||||
|
||||
func init() {
|
||||
var Config types.CLIConfig
|
||||
var DevNull types.DevNull
|
||||
|
||||
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)
|
||||
|
||||
config.InitConfig()
|
||||
rootCmd = root.NewRootCmd()
|
||||
}
|
||||
|
||||
// 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
|
||||
// goes wrong. This will exit the cli container during pipeline build and fail that stage.
|
||||
func main() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
err := rootCmd.Execute()
|
||||
config.SaveConfig()
|
||||
if err != nil {
|
||||
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
|
||||
## explicit; go 1.16
|
||||
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
|
||||
## explicit; go 1.11
|
||||
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/ssh
|
||||
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
|
||||
## explicit; go 1.17
|
||||
golang.org/x/net/context
|
||||
|
||||
Reference in New Issue
Block a user