Adjusted Project Structure, added environment support, added 90% spconfig functionality

This commit is contained in:
luke-hagar-sp
2023-02-08 21:09:58 -06:00
parent 523732c760
commit 1e438f8a05
63 changed files with 3371 additions and 1542 deletions

View File

@@ -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")
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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")
if config.GetAuthType() != "pat" {
config.SetAuthType("pat")
color.Blue("authentication method set to pat")
}
case "oauth":
viper.Set("authtype", "oauth")
if config.GetAuthType() != "oauth" {
config.SetAuthType("oauth")
color.Blue("authentication method set to 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
}
return fmt.Errorf("invalid selection")
}
return nil

View File

@@ -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
},
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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")

View File

@@ -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

View File

@@ -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
View 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
}

View File

@@ -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
View 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
}

View File

@@ -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

View File

@@ -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
View 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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
},
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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
}

View File

@@ -1,5 +0,0 @@
package errors
import "fmt"
var ErrAccessTokenExpired = fmt.Errorf("accesstoken is expired")

64
internal/output/output.go Normal file
View 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
View 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
View 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"`
}

View 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
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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())
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
View File

@@ -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
View File

@@ -0,0 +1 @@
.idea

4
vendor/github.com/gocarina/gocsv/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,4 @@
language: go
arch:
- amd64
- ppc64le

21
vendor/github.com/gocarina/gocsv/LICENSE generated vendored Normal file
View 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
View 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
[![GoDoc](https://godoc.org/github.com/gocarina/gocsv?status.png)](https://godoc.org/github.com/gocarina/gocsv)
[![Build Status](https://travis-ci.org/gocarina/gocsv.svg?branch=master)](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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -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