diff --git a/cmd/harbor/root/project/robot.go b/cmd/harbor/root/project/robot.go index 49d1d189..12a8a619 100644 --- a/cmd/harbor/root/project/robot.go +++ b/cmd/harbor/root/project/robot.go @@ -13,9 +13,10 @@ func Robot() *cobra.Command { } cmd.AddCommand( robot.ListRobotCommand(), - robot.DeleteCommand(), - robot.ViewCommand(), - robot.CreateRobot(), + robot.DeleteRobotCommand(), + robot.ViewRobotCommand(), + robot.CreateRobotCommand(), + robot.UpdateRobotCommand(), ) return cmd diff --git a/cmd/harbor/root/project/robot/create.go b/cmd/harbor/root/project/robot/create.go index 3e24d2d3..87b6f9a0 100644 --- a/cmd/harbor/root/project/robot/create.go +++ b/cmd/harbor/root/project/robot/create.go @@ -14,8 +14,8 @@ import ( "github.com/spf13/viper" ) -// CreateProjectCommand creates a new `harbor create project` command -func CreateRobot() *cobra.Command { +// to-do add json file as input and getting json file as output from input. +func CreateRobotCommand() *cobra.Command { var ( opts create.CreateView projectName string diff --git a/cmd/harbor/root/project/robot/delete.go b/cmd/harbor/root/project/robot/delete.go index 105b9196..09015bdf 100644 --- a/cmd/harbor/root/project/robot/delete.go +++ b/cmd/harbor/root/project/robot/delete.go @@ -9,12 +9,12 @@ import ( "github.com/spf13/cobra" ) -// NewGetRegistryCommand creates a new `harbor get registry` command -func DeleteCommand() *cobra.Command { +// to-do improve DeleteRobotCommand and multi delete +func DeleteRobotCommand() *cobra.Command { cmd := &cobra.Command{ Use: "delete [robotID]", Short: "delete robot by id", - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { if len(args) == 1 { robotID, err := strconv.ParseInt(args[0], 10, 64) diff --git a/cmd/harbor/root/project/robot/list.go b/cmd/harbor/root/project/robot/list.go index 9730daad..2afe9c38 100644 --- a/cmd/harbor/root/project/robot/list.go +++ b/cmd/harbor/root/project/robot/list.go @@ -13,11 +13,11 @@ import ( "github.com/spf13/viper" ) -// ListRobotCommand creates a new `harbor robot list` command +// ListRobotCommand creates a new `harbor project robot list` command func ListRobotCommand() *cobra.Command { var ( - query string - opts api.ListFlags + query string + opts api.ListFlags ) projectQString := constants.ProjectQString @@ -29,7 +29,7 @@ func ListRobotCommand() *cobra.Command { if len(args) > 0 { opts.Q = projectQString + args[0] } else { - projectID := prompt.GetProjectIDFromUser() + projectID := prompt.GetProjectIDFromUser() opts.Q = projectQString + strconv.FormatInt(projectID, 10) } diff --git a/cmd/harbor/root/project/robot/update.go b/cmd/harbor/root/project/robot/update.go new file mode 100644 index 00000000..53cb1ce5 --- /dev/null +++ b/cmd/harbor/root/project/robot/update.go @@ -0,0 +1,117 @@ +package robot + +import ( + "strconv" + + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + "github.com/goharbor/harbor-cli/pkg/views/robot/update" + log "github.com/sirupsen/logrus" + + "github.com/spf13/cobra" +) + +// to-do complete UpdateRobotCommand +func UpdateRobotCommand() *cobra.Command { + var ( + robotID int64 + opts update.UpdateView + all bool + ) + + cmd := &cobra.Command{ + Use: "update [robotID]", + Short: "update robot by id", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + if len(args) == 1 { + robotID, err = strconv.ParseInt(args[0], 10, 64) + if err != nil { + log.Errorf("failed to parse robot ID: %v", err) + } + + } else { + projectID := prompt.GetProjectIDFromUser() + robotID = prompt.GetRobotIDFromUser(projectID) + } + + robot, err := api.GetRobot(robotID) + bot := robot.Payload + + opts = update.UpdateView{ + CreationTime: bot.CreationTime, + Description: bot.Description, + Disable: bot.Disable, + Duration: bot.Duration, + Editable: bot.Editable, + ID: bot.ID, + Level: bot.Level, + Name: bot.Name, + Secret: bot.Secret, + } + + // declare empty permissions to hold permissions + permissions := []models.Permission{} + + if all { + perms, _ := api.GetPermissions() + permission := perms.Payload.Project + + choices := []models.Permission{} + for _, perm := range permission { + choices = append(choices, *perm) + } + permissions = choices + } else { + permissions = prompt.GetRobotPermissionsFromUser() + } + + // []Permission to []*Access + var accesses []*models.Access + for _, perm := range permissions { + access := &models.Access{ + Action: perm.Action, + Resource: perm.Resource, + } + accesses = append(accesses, access) + } + // convert []models.permission to []*model.Access + perm := &update.RobotPermission{ + Kind: bot.Permissions[0].Kind, + Namespace: bot.Permissions[0].Namespace, + Access: accesses, + } + opts.Permissions = []*update.RobotPermission{perm} + + err = updateRobotView(&opts) + if err != nil { + log.Errorf("failed to Update robot") + } + }, + } + + flags := cmd.Flags() + flags.BoolVarP( + &all, + "all-permission", + "a", + false, + "Select all permissions for the robot account", + ) + flags.StringVarP(&opts.Name, "name", "", "", "name of the robot account") + flags.StringVarP(&opts.Description, "description", "", "", "description of the robot account") + flags.Int64VarP(&opts.Duration, "duration", "", 0, "set expiration of robot account in days") + + return cmd +} + +func updateRobotView(updateView *update.UpdateView) error { + if updateView == nil { + updateView = &update.UpdateView{} + } + + update.UpdateRobotView(updateView) + return api.UpdateRobot(updateView) +} diff --git a/cmd/harbor/root/project/robot/view.go b/cmd/harbor/root/project/robot/view.go index bd38e5ed..e138253a 100644 --- a/cmd/harbor/root/project/robot/view.go +++ b/cmd/harbor/root/project/robot/view.go @@ -1,6 +1,7 @@ package robot import ( + "os" "strconv" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/robot" @@ -12,8 +13,8 @@ import ( "github.com/spf13/cobra" ) -// ViewCommand creates a new `harbor project robot view` command -func ViewCommand() *cobra.Command { +// handle robot view with interactive like in list command. +func ViewRobotCommand() *cobra.Command { cmd := &cobra.Command{ Use: "view [robotID]", Short: "get robot by id", @@ -29,6 +30,7 @@ func ViewCommand() *cobra.Command { robot, err = api.GetRobot(robotID) if err != nil { log.Errorf("failed to List robots") + os.Exit(1) } } robots := []*models.Robot{robot.Payload} diff --git a/pkg/api/robot_handler.go b/pkg/api/robot_handler.go index 89bff9e5..669bbe01 100644 --- a/pkg/api/robot_handler.go +++ b/pkg/api/robot_handler.go @@ -6,6 +6,7 @@ import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/robot/create" + "github.com/goharbor/harbor-cli/pkg/views/robot/update" log "github.com/sirupsen/logrus" ) @@ -59,7 +60,7 @@ func DeleteRobot(robotID int64) error { return nil } -func CreateRobot(opts create.CreateView) (*robot.CreateRobotCreated ,error) { +func CreateRobot(opts create.CreateView) (*robot.CreateRobotCreated, error) { ctx, client, err := utils.ContextWithClient() if err != nil { return nil, err @@ -79,7 +80,7 @@ func CreateRobot(opts create.CreateView) (*robot.CreateRobotCreated ,error) { } convertedPerms = append(convertedPerms, convertedPerm) } - response, err := client.Robot.CreateRobot( + response, err := client.Robot.CreateRobot( ctx, &robot.CreateRobotParams{ Robot: &models.RobotCreate{ @@ -93,13 +94,62 @@ func CreateRobot(opts create.CreateView) (*robot.CreateRobotCreated ,error) { }, ) if err != nil { - return nil,err + return nil, err } log.Info("robot created successfully.") return response, nil } +// update robot with robotID +func UpdateRobot(opts *update.UpdateView) error { + ctx, client, err := utils.ContextWithClient() + if err != nil { + log.Errorf("Error: %v", err) + return err + } + + log.Println(opts) + + // Create a slice to store converted permissions + permissions := opts.Permissions + convertedPerms := make([]*models.RobotPermission, 0, len(permissions)) + + kind := "project" + // Loop through original permissions and convert them + for _, perm := range permissions { + convertedPerm := &models.RobotPermission{ + Access: perm.Access, + Kind: kind, + Namespace: opts.Permissions[0].Namespace, + } + convertedPerms = append(convertedPerms, convertedPerm) + } + _, err = client.Robot.UpdateRobot( + ctx, + &robot.UpdateRobotParams{ + Robot: &models.Robot{ + Description: opts.Description, + Duration: opts.Duration, + Editable: opts.Editable, + Disable: opts.Disable, + ID: opts.ID, + Level: opts.Level, + Name: opts.Name, + Permissions: convertedPerms, + }, + RobotID: opts.ID, + }, + ) + if err != nil { + log.Errorf("Error in updating Robot: %v", err) + return err + } + + log.Info("robot updated successfully.") + return nil +} + func GetPermissions() (*permissions.GetPermissionsOK, error) { ctx, client, err := utils.ContextWithClient() if err != nil { diff --git a/pkg/prompt/prompt.go b/pkg/prompt/prompt.go index 71631b71..f9aa51d7 100644 --- a/pkg/prompt/prompt.go +++ b/pkg/prompt/prompt.go @@ -1,8 +1,11 @@ package prompt import ( + "strconv" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/constants" aview "github.com/goharbor/harbor-cli/pkg/views/artifact/select" tview "github.com/goharbor/harbor-cli/pkg/views/artifact/tags/select" pview "github.com/goharbor/harbor-cli/pkg/views/project/select" @@ -104,3 +107,15 @@ func GetRobotPermissionsFromUser() []models.Permission { }() return <-permissions } + +func GetRobotIDFromUser(projectID int64) int64 { + robotID := make(chan int64) + var opts api.ListFlags + opts.Q = constants.ProjectQString + strconv.FormatInt(projectID, 10) + + go func() { + response, _ := api.ListRobot(opts) + robotView.ListRobot(response.Payload, robotID) + }() + return <-robotID +} diff --git a/pkg/views/base/multiselect/model.go b/pkg/views/base/multiselect/model.go index b6396a7b..fe1b61eb 100644 --- a/pkg/views/base/multiselect/model.go +++ b/pkg/views/base/multiselect/model.go @@ -2,6 +2,8 @@ package multiselect import ( "fmt" + "log" + "os" "strings" "github.com/charmbracelet/bubbles/viewport" @@ -19,9 +21,9 @@ var ( return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) }() - selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("43")) - itemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("46")) - blockStyle = lipgloss.NewStyle(). + selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("43")) + itemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("46")) + blockStyle = lipgloss.NewStyle(). Background(lipgloss.Color("81")). Foreground(lipgloss.Color("#000000")). Bold(true). @@ -170,7 +172,6 @@ func (m Model) listView() string { return s } - func (m Model) GetSelectedPermissions() *[]models.Permission { selectedPermissions := make([]models.Permission, 0, len(m.selected)) for index := range m.selected { diff --git a/pkg/views/robot/list/view.go b/pkg/views/robot/list/view.go index 5a1d39bf..1a04972e 100644 --- a/pkg/views/robot/list/view.go +++ b/pkg/views/robot/list/view.go @@ -16,6 +16,7 @@ import ( ) var columns = []table.Column{ + {Title: "ID", Width: 4}, {Title: "Name", Width: 30}, {Title: "Status", Width: 18}, {Title: "Permissions", Width: 12}, @@ -47,10 +48,11 @@ func ListRobots(robots []*models.Robot) { createdTime, _ := utils.FormatCreatedTime(robot.CreationTime.String()) rows = append(rows, table.Row{ - robot.Name, // Project Name - enabledStatus, // Access Level + strconv.FormatInt(robot.ID, 10), + robot.Name, + enabledStatus, TotalPermissions, - createdTime, // Creation Time + createdTime, expires, robot.Description, }) diff --git a/pkg/views/robot/select/view.go b/pkg/views/robot/select/view.go index cc76dceb..01d9f186 100644 --- a/pkg/views/robot/select/view.go +++ b/pkg/views/robot/select/view.go @@ -2,10 +2,14 @@ package robot import ( "fmt" + "log" + "os" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/views/base/multiselect" + "github.com/goharbor/harbor-cli/pkg/views/base/selection" ) func ListPermissions(perms *models.Permissions, ch chan<- []models.Permission) { @@ -21,10 +25,33 @@ func ListPermissions(perms *models.Permissions, ch chan<- []models.Permission) { m := multiselect.NewModel(choices, selects) - _, err := tea.NewProgram(m).Run() + _, err := tea.NewProgram(m, tea.WithAltScreen()).Run() if err != nil { fmt.Println("Error running program:", err) } // Get selected permissions ch <- *selects } + +func ListRobot(robots []*models.Robot, choice chan<- int64) { + itemsList := make([]list.Item, len(robots)) + + items := map[string]int64{} + + for i, r := range robots { + items[r.Name] = r.ID + itemsList[i] = selection.Item(r.Name) + } + + m := selection.NewModel(itemsList, "Robot") + + p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() + if err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } + + if p, ok := p.(selection.Model); ok { + choice <- items[p.Choice] + } +} diff --git a/pkg/views/robot/update/view.go b/pkg/views/robot/update/view.go new file mode 100644 index 00000000..a91efade --- /dev/null +++ b/pkg/views/robot/update/view.go @@ -0,0 +1,74 @@ +package update + +import ( + "errors" + "strconv" + + "github.com/charmbracelet/huh" + "github.com/go-openapi/strfmt" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + log "github.com/sirupsen/logrus" +) + +type UpdateView struct { + CreationTime strfmt.DateTime `json:"creation_time,omitempty"` + Description string `json:"description,omitempty"` + Disable bool `json:"disable,omitempty"` + Duration int64 `json:"duration,omitempty"` + Editable bool `json:"editable"` + ExpiresAt int64 `json:"expires_at,omitempty"` + ID int64 `json:"id,omitempty"` + Level string `json:"level,omitempty"` + Name string `json:"name,omitempty"` + Permissions []*RobotPermission `json:"permissions"` + Secret string `json:"secret,omitempty"` + UpdateTime strfmt.DateTime `json:"update_time,omitempty"` +} + +type RobotPermission struct { + Access []*models.Access `json:"access"` + Kind string `json:"kind,omitempty"` + Namespace string `json:"namespace,omitempty"` +} + +type Access struct { + Action string `json:"action,omitempty"` + Effect string `json:"effect,omitempty"` + Resource string `json:"resource,omitempty"` +} + +func UpdateRobotView(updateView *UpdateView) { + var duration string + duration = strconv.FormatInt(updateView.Duration, 10) + + theme := huh.ThemeCharm() + err := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Description"). + Value(&updateView.Description), + huh.NewInput(). + Title("Expiration"). + Value(&duration). + Validate(func(str string) error { + if str == "" { + return errors.New("Expiration cannot be empty") + } + dur, err := strconv.ParseInt(str, 10, 64) + if err != nil { + return errors.New("invalid expiration time: Enter expiration time in days") + } + updateView.Duration = dur + return nil + }), + huh.NewConfirm(). + Title("Disable"). + Value(&updateView.Disable). + Affirmative("yes"). + Negative("no"), + ), + ).WithTheme(theme).Run() + if err != nil { + log.Fatal(err) + } +}