diff --git a/cmd/profile.go b/cmd/profile.go index 6214518..2d038c3 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -20,15 +20,12 @@ import ( "errors" "fmt" "net/url" - "os" "pb/pkg/config" "pb/pkg/model/credential" "pb/pkg/model/defaultprofile" "time" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/muesli/termenv" "github.com/spf13/cobra" ) @@ -91,68 +88,67 @@ var AddProfileCmd = &cobra.Command{ } return cobra.MaximumNArgs(4)(cmd, args) }, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) + } + startTime := time.Now() + var commandError error + + // Parsing input and handling errors name := args[0] url, err := url.Parse(args[1]) if err != nil { - return err + commandError = fmt.Errorf("error parsing URL: %s", err) + cmd.Annotations["error"] = commandError.Error() + return commandError } - var username string - var password string - + var username, password string if len(args) < 4 { _m, err := tea.NewProgram(credential.New()).Run() if err != nil { - fmt.Printf("Alas, there's been an error: %v", err) - os.Exit(1) + commandError = fmt.Errorf("error reading credentials: %s", err) + cmd.Annotations["error"] = commandError.Error() + return commandError } m := _m.(credential.Model) - username, password = m.Values() } else { username = args[2] password = args[3] } - profile := config.Profile{ - URL: url.String(), - Username: username, - Password: password, - } - + profile := config.Profile{URL: url.String(), Username: username, Password: password} fileConfig, err := config.ReadConfigFromFile() if err != nil { - // create new file newConfig := config.Config{ - Profiles: map[string]config.Profile{ - name: profile, - }, + Profiles: map[string]config.Profile{name: profile}, DefaultProfile: name, } err = config.WriteConfigToFile(&newConfig) - return err - } - if fileConfig.Profiles == nil { - fileConfig.Profiles = make(map[string]config.Profile) - } - fileConfig.Profiles[name] = profile - if fileConfig.DefaultProfile == "" { - fileConfig.DefaultProfile = name + commandError = err + } else { + if fileConfig.Profiles == nil { + fileConfig.Profiles = make(map[string]config.Profile) + } + fileConfig.Profiles[name] = profile + if fileConfig.DefaultProfile == "" { + fileConfig.DefaultProfile = name + } + commandError = config.WriteConfigToFile(fileConfig) } - err = config.WriteConfigToFile(fileConfig) - if err != nil { - fmt.Printf("add profile %s failed\n, err: %v\n", StyleBold.Render(name), err) - return err + cmd.Annotations["executionTime"] = time.Since(startTime).String() + if commandError != nil { + cmd.Annotations["error"] = commandError.Error() + return commandError } - fmt.Printf("Added profile %s\n", StyleBold.Render(name)) if outputFormat == "json" { return outputResult(profile) } fmt.Printf("Profile %s added successfully\n", name) - return nil }, } @@ -163,29 +159,43 @@ var RemoveProfileCmd = &cobra.Command{ Example: " pb profile remove local_parseable", Args: cobra.ExactArgs(1), Short: "Delete a profile", - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) + } + startTime := time.Now() + name := args[0] fileConfig, err := config.ReadConfigFromFile() if err != nil { - return nil + cmd.Annotations["error"] = fmt.Sprintf("error reading config: %s", err) + return err } _, exists := fileConfig.Profiles[name] - if exists { - delete(fileConfig.Profiles, name) - if len(fileConfig.Profiles) == 0 { - fileConfig.DefaultProfile = "" - } + if !exists { + msg := fmt.Sprintf("No profile found with the name: %s", name) + cmd.Annotations["error"] = msg + fmt.Println(msg) + return nil + } - config.WriteConfigToFile(fileConfig) - if outputFormat == "json" { - return outputResult(fmt.Sprintf("Deleted profile %s", name)) - } - fmt.Printf("Deleted profile %s\n", StyleBold.Render(name)) - } else { - fmt.Printf("No profile found with the name: %s", StyleBold.Render(name)) + delete(fileConfig.Profiles, name) + if len(fileConfig.Profiles) == 0 { + fileConfig.DefaultProfile = "" } + commandError := config.WriteConfigToFile(fileConfig) + cmd.Annotations["executionTime"] = time.Since(startTime).String() + if commandError != nil { + cmd.Annotations["error"] = commandError.Error() + return commandError + } + + if outputFormat == "json" { + return outputResult(fmt.Sprintf("Deleted profile %s", name)) + } + fmt.Printf("Deleted profile %s\n", name) return nil }, } @@ -195,46 +205,54 @@ var DefaultProfileCmd = &cobra.Command{ Args: cobra.MaximumNArgs(1), Short: "Set default profile to use with all commands", Example: " pb profile default local_parseable", - RunE: func(_ *cobra.Command, args []string) error { - var name string + RunE: func(cmd *cobra.Command, args []string) error { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) + } + startTime := time.Now() fileConfig, err := config.ReadConfigFromFile() if err != nil { - return nil + cmd.Annotations["error"] = fmt.Sprintf("error reading config: %s", err) + return err } + var name string if len(args) > 0 { name = args[0] } else { model := defaultprofile.New(fileConfig.Profiles) _m, err := tea.NewProgram(model).Run() if err != nil { - fmt.Printf("Alas, there's been an error: %v", err) - os.Exit(1) + cmd.Annotations["error"] = fmt.Sprintf("error selecting default profile: %s", err) + return err } m := _m.(defaultprofile.Model) - termenv.DefaultOutput().ClearLines(lipgloss.Height(model.View()) - 1) - if m.Success { - name = m.Choice - } else { + if !m.Success { return nil } + name = m.Choice } _, exists := fileConfig.Profiles[name] - if exists { - fileConfig.DefaultProfile = name - } else { - name = lipgloss.NewStyle().Bold(true).Render(name) - err := fmt.Sprintf("profile %s does not exist", StyleBold.Render(name)) - return errors.New(err) + if !exists { + commandError := fmt.Sprintf("profile %s does not exist", name) + cmd.Annotations["error"] = commandError + return errors.New(commandError) + } + + fileConfig.DefaultProfile = name + commandError := config.WriteConfigToFile(fileConfig) + cmd.Annotations["executionTime"] = time.Since(startTime).String() + if commandError != nil { + cmd.Annotations["error"] = commandError.Error() + return commandError } - config.WriteConfigToFile(fileConfig) if outputFormat == "json" { return outputResult(fmt.Sprintf("%s is now set as default profile", name)) } - fmt.Printf("%s is now set as default profile\n", StyleBold.Render(name)) + fmt.Printf("%s is now set as default profile\n", name) return nil }, } @@ -244,45 +262,32 @@ var ListProfileCmd = &cobra.Command{ Short: "List all added profiles", Example: " pb profile list", RunE: func(cmd *cobra.Command, _ []string) error { - // Record the start time of the command execution + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) + } startTime := time.Now() - // Initialize a variable to capture errors - var commandError error - - // Read the configuration from file fileConfig, err := config.ReadConfigFromFile() if err != nil { - commandError = fmt.Errorf("rror reading config: %s", err) - cmd.Annotations["error"] = commandError.Error() // Store error in annotations - return commandError // Return the error so it's handled properly - } - - if len(fileConfig.Profiles) != 0 { - println() + cmd.Annotations["error"] = fmt.Sprintf("error reading config: %s", err) + return err } if outputFormat == "json" { - if err := outputResult(fileConfig.Profiles); err != nil { - commandError = fmt.Errorf("error outputting result: %s", err) - cmd.Annotations["error"] = commandError.Error() // Store error in annotations - return commandError // Return the error + commandError := outputResult(fileConfig.Profiles) + cmd.Annotations["executionTime"] = time.Since(startTime).String() + if commandError != nil { + cmd.Annotations["error"] = commandError.Error() + return commandError } - return nil // No error, exit normally + return nil } - row := 0 for key, value := range fileConfig.Profiles { item := ProfileListItem{key, value.URL, value.Username} fmt.Println(item.Render(fileConfig.DefaultProfile == key)) - row++ - fmt.Println() } - - // Store the execution duration as a field for PostRunE to access cmd.Annotations["executionTime"] = time.Since(startTime).String() - - // If there were no errors, return nil return nil }, } diff --git a/cmd/user.go b/cmd/user.go index 43fc374..1db904b 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -23,6 +23,7 @@ import ( internalHTTP "pb/pkg/http" "strings" "sync" + "time" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" @@ -47,11 +48,18 @@ var addUser = &cobra.Command{ Short: "Add a new user", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + startTime := time.Now() + cmd.Annotations = make(map[string]string) // Initialize Annotations map + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + name := args[0] client := internalHTTP.DefaultClient(&DefaultProfile) users, err := fetchUsers(&client) if err != nil { + cmd.Annotations["error"] = err.Error() return err } @@ -59,6 +67,7 @@ var addUser = &cobra.Command{ return user.ID == name }) { fmt.Println("user already exists") + cmd.Annotations["error"] = "user already exists" return nil } @@ -69,6 +78,7 @@ var addUser = &cobra.Command{ // fetch the role names on the server var rolesOnServer []string if err := fetchRoles(&client, &rolesOnServer); err != nil { + cmd.Annotations["error"] = err.Error() return err } rolesOnServerArr := strings.Join(rolesOnServer, " ") @@ -77,7 +87,8 @@ var addUser = &cobra.Command{ for idx, role := range rolesToSetArr { rolesToSetArr[idx] = strings.TrimSpace(role) if !strings.Contains(rolesOnServerArr, rolesToSetArr[idx]) { - fmt.Printf("role %s doesn't exist, please create a role using `pb role add %s`\n", rolesToSetArr[idx], rolesToSetArr[idx]) + fmt.Printf("role %s doesn't exist, please create a role using pb role add %s\n", rolesToSetArr[idx], rolesToSetArr[idx]) + cmd.Annotations["error"] = fmt.Sprintf("role %s doesn't exist", rolesToSetArr[idx]) return nil } } @@ -87,16 +98,19 @@ var addUser = &cobra.Command{ putBody = bytes.NewBuffer([]byte(putBodyJSON)) req, err := client.NewRequest("POST", "user/"+name, putBody) if err != nil { + cmd.Annotations["error"] = err.Error() return err } resp, err := client.Client.Do(req) if err != nil { + cmd.Annotations["error"] = err.Error() return err } bytes, err := io.ReadAll(resp.Body) if err != nil { + cmd.Annotations["error"] = err.Error() return err } body := string(bytes) @@ -104,8 +118,10 @@ var addUser = &cobra.Command{ if resp.StatusCode == 200 { fmt.Printf("Added user: %s \nPassword is: %s\nRole(s) assigned: %s\n", name, body, rolesToSet) + cmd.Annotations["error"] = "none" } else { fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + cmd.Annotations["error"] = fmt.Sprintf("request failed with status code %s", resp.Status) } return nil @@ -117,36 +133,41 @@ var AddUserCmd = func() *cobra.Command { return addUser }() +// Similar changes for RemoveUserCmd var RemoveUserCmd = &cobra.Command{ Use: "remove user-name", Aliases: []string{"rm"}, Example: " pb user remove bob", Short: "Delete a user", Args: cobra.ExactArgs(1), - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + name := args[0] client := internalHTTP.DefaultClient(&DefaultProfile) req, err := client.NewRequest("DELETE", "user/"+name, nil) if err != nil { + cmd.Annotations["error"] = err.Error() return err } resp, err := client.Client.Do(req) if err != nil { + cmd.Annotations["error"] = err.Error() return err } if resp.StatusCode == 200 { fmt.Printf("Removed user %s\n", StyleBold.Render(name)) + cmd.Annotations["error"] = "none" } else { - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - body := string(bytes) - defer resp.Body.Close() - - fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + body, _ := io.ReadAll(resp.Body) + fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, string(body)) + cmd.Annotations["error"] = fmt.Sprintf("request failed with status code %s", resp.Status) } return nil @@ -163,12 +184,18 @@ var SetUserRoleCmd = &cobra.Command{ } return nil }, - RunE: func(_ *cobra.Command, args []string) error { - name := args[0] + RunE: func(cmd *cobra.Command, args []string) error { + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + name := args[0] client := internalHTTP.DefaultClient(&DefaultProfile) users, err := fetchUsers(&client) if err != nil { + cmd.Annotations["error"] = err.Error() return err } @@ -176,25 +203,24 @@ var SetUserRoleCmd = &cobra.Command{ return user.ID == name }) { fmt.Printf("user doesn't exist. Please create the user with `pb user add %s`\n", name) + cmd.Annotations["error"] = "user does not exist" return nil } - // fetch all the roles to be applied to this user rolesToSet := args[1] rolesToSetArr := strings.Split(rolesToSet, ",") - - // fetch the role names on the server var rolesOnServer []string if err := fetchRoles(&client, &rolesOnServer); err != nil { + cmd.Annotations["error"] = err.Error() return err } rolesOnServerArr := strings.Join(rolesOnServer, " ") - // validate if roles to be applied are actually present on the server for idx, role := range rolesToSetArr { rolesToSetArr[idx] = strings.TrimSpace(role) if !strings.Contains(rolesOnServerArr, rolesToSetArr[idx]) { fmt.Printf("role %s doesn't exist, please create a role using `pb role add %s`\n", rolesToSetArr[idx], rolesToSetArr[idx]) + cmd.Annotations["error"] = fmt.Sprintf("role %s doesn't exist", rolesToSetArr[idx]) return nil } } @@ -204,16 +230,19 @@ var SetUserRoleCmd = &cobra.Command{ putBody = bytes.NewBuffer([]byte(putBodyJSON)) req, err := client.NewRequest("PUT", "user/"+name+"/role", putBody) if err != nil { + cmd.Annotations["error"] = err.Error() return err } resp, err := client.Client.Do(req) if err != nil { + cmd.Annotations["error"] = err.Error() return err } bytes, err := io.ReadAll(resp.Body) if err != nil { + cmd.Annotations["error"] = err.Error() return err } body := string(bytes) @@ -221,8 +250,10 @@ var SetUserRoleCmd = &cobra.Command{ if resp.StatusCode == 200 { fmt.Printf("Added role(s) %s to user %s\n", rolesToSet, name) + cmd.Annotations["error"] = "none" } else { fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + cmd.Annotations["error"] = fmt.Sprintf("request failed with status code %s", resp.Status) } return nil @@ -234,14 +265,21 @@ var ListUserCmd = &cobra.Command{ Short: "List all users", Example: " pb user list", RunE: func(cmd *cobra.Command, _ []string) error { + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + client := internalHTTP.DefaultClient(&DefaultProfile) users, err := fetchUsers(&client) if err != nil { + cmd.Annotations["error"] = err.Error() return err } roleResponses := make([]struct { - data []string // Collects roles as strings for text output + data []string err error }, len(users)) @@ -255,7 +293,6 @@ var ListUserCmd = &cobra.Command{ var userRolesData UserRoleData userRolesData, out.err = fetchUserRoles(client, userID) if out.err == nil { - // Collect role names for this user for role := range userRolesData { out.data = append(out.data, role) } @@ -266,13 +303,12 @@ var ListUserCmd = &cobra.Command{ wsg.Wait() - // Get the output format, defaulting to empty (existing behavior) outputFormat, err := cmd.Flags().GetString("output") if err != nil { + cmd.Annotations["error"] = err.Error() return err } - // JSON output if specified if outputFormat == "json" { usersWithRoles := make([]map[string]interface{}, len(users)) for idx, user := range users { @@ -283,13 +319,14 @@ var ListUserCmd = &cobra.Command{ } jsonOutput, err := json.MarshalIndent(usersWithRoles, "", " ") if err != nil { + cmd.Annotations["error"] = err.Error() return fmt.Errorf("failed to marshal JSON output: %w", err) } fmt.Println(string(jsonOutput)) + cmd.Annotations["error"] = "none" return nil } - // Text output if specified if outputFormat == "text" { fmt.Println() for idx, user := range users { @@ -302,10 +339,10 @@ var ListUserCmd = &cobra.Command{ } } fmt.Println() + cmd.Annotations["error"] = "none" return nil } - // Default output (existing layout) fmt.Println() for idx, user := range users { roles := roleResponses[idx] @@ -321,6 +358,7 @@ var ListUserCmd = &cobra.Command{ } fmt.Println() + cmd.Annotations["error"] = "none" return nil }, } diff --git a/main.go b/main.go index 4681fd8..6dbd737 100644 --- a/main.go +++ b/main.go @@ -65,7 +65,7 @@ var profile = &cobra.Command{ Short: "Manage different Parseable targets", Long: "\nuse profile command to configure different Parseable instances. Each profile takes a URL and credentials.", PersistentPostRun: func(cmd *cobra.Command, args []string) { - go analytics.PostRunAnalytics(cmd, args) + analytics.PostRunAnalytics(cmd, args) }, } @@ -74,6 +74,9 @@ var user = &cobra.Command{ Short: "Manage users", Long: "\nuser command is used to manage users.", PersistentPreRunE: cmd.PreRunDefaultProfile, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + analytics.PostRunAnalytics(cmd, args) + }, } var role = &cobra.Command{ diff --git a/pkg/analytics/analytics.go b/pkg/analytics/analytics.go index 193f8fb..0cbebad 100644 --- a/pkg/analytics/analytics.go +++ b/pkg/analytics/analytics.go @@ -1,10 +1,12 @@ package analytics import ( + "bytes" "encoding/json" "errors" "fmt" "io" + "net/http" "os" "os/exec" "path/filepath" @@ -150,7 +152,6 @@ func PostRunAnalytics(cmd *cobra.Command, args []string) { // sendEvent is a placeholder function to simulate sending an event after command execution. func sendEvent(commandName string, arguments []string, errors *string, executionStatus string, flags map[string]string) error { - uuid, err := ReadUUID() if err != nil { return fmt.Errorf("could not load UUID: %v", err) @@ -189,20 +190,38 @@ func sendEvent(commandName string, arguments []string, errors *string, execution ExecutionStatus: executionStatus, } + event.Profile.Password = "" + // Marshal the event to JSON for sending eventJSON, err := json.Marshal(event) if err != nil { return fmt.Errorf("failed to marshal event JSON: %v", err) } - // Simulate sending the event (print or make an HTTP request) - fmt.Println("Sending event:", string(eventJSON)) + // Define the target URL for the HTTP request + url := "https://ingestor.demo.parseable.com/api/v1/logstream/analytics-test" + + // Create the HTTP POST request + req, err := http.NewRequest("POST", url, bytes.NewBuffer(eventJSON)) + if err != nil { + return fmt.Errorf("failed to create HTTP request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + req.SetBasicAuth("admin", "admin") + // Execute the HTTP request + resp, err := httpClient.Client.Do(req) + if err != nil { + return fmt.Errorf("failed to send event: %v", err) + } + defer resp.Body.Close() - // err = sendHTTPRequest("POST", "https://example.com/events", eventJSON) - // if err != nil { - // return fmt.Errorf("failed to send event: %v", err) - // } + // Check for a non-2xx status code + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("received non-2xx response: %v", resp.Status) + } + fmt.Println("Event sent successfully:", string(eventJSON)) return nil } diff --git a/pkg/config/config.go b/pkg/config/config.go index 62d0d95..c57a9cc 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -49,9 +49,9 @@ type Config struct { // Profile is the struct that holds the profile configuration type Profile struct { - URL string - Username string - Password string + URL string `json:"url"` + Username string `json:"username"` + Password string `json:"password,omitempty"` } func (p *Profile) GrpcAddr(port string) string {