diff --git a/cmd/harbor/root/cmd.go b/cmd/harbor/root/cmd.go index 542185c9..f5308915 100644 --- a/cmd/harbor/root/cmd.go +++ b/cmd/harbor/root/cmd.go @@ -7,6 +7,7 @@ import ( "github.com/goharbor/harbor-cli/cmd/harbor/root/project" "github.com/goharbor/harbor-cli/cmd/harbor/root/registry" + repositry "github.com/goharbor/harbor-cli/cmd/harbor/root/repository" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -100,6 +101,7 @@ harbor help LoginCommand(), project.Project(), registry.Registry(), + repositry.Repository(), ) return root diff --git a/cmd/harbor/root/repository/cmd.go b/cmd/harbor/root/repository/cmd.go new file mode 100644 index 00000000..3efa30b6 --- /dev/null +++ b/cmd/harbor/root/repository/cmd.go @@ -0,0 +1,19 @@ +package repository + +import "github.com/spf13/cobra" + +func Repository() *cobra.Command { + cmd := &cobra.Command{ + Use: "repo", + Short: "Manage repositories", + Long: `Manage repositories in Harbor context`, + } + cmd.AddCommand( + ListRepositoryCommand(), + RepoInfoCmd(), + RepoDeleteCmd(), + ) + + return cmd + +} diff --git a/cmd/harbor/root/repository/delete.go b/cmd/harbor/root/repository/delete.go new file mode 100644 index 00000000..d78b7d33 --- /dev/null +++ b/cmd/harbor/root/repository/delete.go @@ -0,0 +1,50 @@ +package repository + +import ( + "context" + + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/repository" + "github.com/goharbor/harbor-cli/pkg/utils" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func RepoDeleteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a repository", + Example: ` harbor repository delete [project_name]/[repository_name]`, + Long: `Delete a repository within a project in Harbor`, + Run: func(cmd *cobra.Command, args []string) { + var err error + if len(args) > 0 { + projectName, repoName := utils.ParseProjectRepo(args[0]) + err = runRepoDelete(projectName, repoName) + } else { + projectName := utils.GetProjectNameFromUser() + repoName := utils.GetRepoNameFromUser(projectName) + err = runRepoDelete(projectName, repoName) + } + if err != nil { + log.Errorf("failed to delete repository: %v", err) + } + }, + } + return cmd +} + +func runRepoDelete(projectName, repoName string) error { + credentialName := viper.GetString("current-credential-name") + client := utils.GetClientByCredentialName(credentialName) + ctx := context.Background() + + _, err := client.Repository.DeleteRepository(ctx, &repository.DeleteRepositoryParams{ProjectName: projectName, RepositoryName: repoName}) + + if err != nil { + return err + } + + log.Infof("Repository %s/%s deleted successfully", projectName, repoName) + return nil +} diff --git a/cmd/harbor/root/repository/info.go b/cmd/harbor/root/repository/info.go new file mode 100644 index 00000000..b9752ec5 --- /dev/null +++ b/cmd/harbor/root/repository/info.go @@ -0,0 +1,52 @@ +package repository + +import ( + "context" + + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/repository" + "github.com/goharbor/harbor-cli/pkg/utils" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func RepoInfoCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "info", + Short: "Get repository information", + Example: ` harbor repo info /`, + Long: `Get information of a particular repository in a project`, + Run: func(cmd *cobra.Command, args []string) { + var err error + if len(args) > 0 { + projectName, repoName := utils.ParseProjectRepo(args[0]) + err = runRepoInfo(projectName, repoName) + } else { + projectName := utils.GetProjectNameFromUser() + repoName := utils.GetRepoNameFromUser(projectName) + err = runRepoInfo(projectName, repoName) + } + if err != nil { + log.Errorf("failed to get repository information: %v", err) + } + + }, + } + + return cmd +} + +func runRepoInfo(projectName, repoName string) error { + credentialName := viper.GetString("current-credential-name") + client := utils.GetClientByCredentialName(credentialName) + ctx := context.Background() + + response, err := client.Repository.GetRepository(ctx, &repository.GetRepositoryParams{ProjectName: projectName, RepositoryName: repoName}) + + if err != nil { + return err + } + + utils.PrintPayloadInJSONFormat(response.Payload) + return nil +} diff --git a/cmd/harbor/root/repository/list.go b/cmd/harbor/root/repository/list.go new file mode 100644 index 00000000..42dc61b1 --- /dev/null +++ b/cmd/harbor/root/repository/list.go @@ -0,0 +1,51 @@ +package repository + +import ( + "context" + + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/repository" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/repository/list" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func ListRepositoryCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "list repositories within a project", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + + if len(args) > 0 { + err = runListRepository(args[0]) + } else { + projectName := utils.GetProjectNameFromUser() + err = runListRepository(projectName) + } + if err != nil { + log.Errorf("failed to list repositories: %v", err) + } + }, + } + + return cmd +} + +func runListRepository(ProjectName string) error { + credentialName := viper.GetString("current-credential-name") + client := utils.GetClientByCredentialName(credentialName) + ctx := context.Background() + + response, err := client.Repository.ListRepositories(ctx, &repository.ListRepositoriesParams{ProjectName: ProjectName}) + + if err != nil { + return err + } + + list.ListRepositories(response.Payload) + return nil + +} diff --git a/cmd/harbor/root/repository/update.go b/cmd/harbor/root/repository/update.go new file mode 100644 index 00000000..50a4378d --- /dev/null +++ b/cmd/harbor/root/repository/update.go @@ -0,0 +1 @@ +package repository diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 5d8b7ed9..31f6bf9f 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -5,13 +5,16 @@ import ( "encoding/json" "fmt" "os" + "strings" "github.com/goharbor/go-client/pkg/harbor" v2client "github.com/goharbor/go-client/pkg/sdk/v2.0/client" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/project" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/registry" + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/repository" pview "github.com/goharbor/harbor-cli/pkg/views/project/select" rview "github.com/goharbor/harbor-cli/pkg/views/registry/select" + repoView "github.com/goharbor/harbor-cli/pkg/views/repository/select" log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) @@ -88,3 +91,29 @@ func GetProjectNameFromUser() string { return <-projectName } + +func GetRepoNameFromUser(projectName string) string { + repositoryName := make(chan string) + + go func() { + credentialName := viper.GetString("current-credential-name") + client := GetClientByCredentialName(credentialName) + ctx := context.Background() + response, err := client.Repository.ListRepositories(ctx, &repository.ListRepositoriesParams{ProjectName: projectName}) + if err != nil { + log.Fatal(err) + } + repoView.RepositoryList(response.Payload, repositoryName) + }() + + return <-repositoryName + +} + +func ParseProjectRepo(projectRepo string) (string, string) { + split := strings.Split(projectRepo, "/") + if len(split) != 2 { + log.Fatalf("invalid project/repository format: %s", projectRepo) + } + return split[0], split[1] +} diff --git a/pkg/views/repository/list/view.go b/pkg/views/repository/list/view.go new file mode 100644 index 00000000..48712bc1 --- /dev/null +++ b/pkg/views/repository/list/view.go @@ -0,0 +1,82 @@ +package list + +import ( + "fmt" + "os" + "strconv" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" +) + +var baseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()).Padding(0, 1) + +type model struct { + table table.Model +} + +var columns = []table.Column{ + {Title: "Name", Width: 24}, + {Title: "Artifacts", Width: 12}, + {Title: "Pulls", Width: 12}, + {Title: "Last Modified Time", Width: 30}, +} + +func (m model) Init() tea.Cmd { + return tea.Quit +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + m.table, cmd = m.table.Update(msg) + return m, cmd +} + +func (m model) View() string { + return baseStyle.Render(m.table.View()) + "\n" +} + +func ListRepositories(repos []*models.Repository) { + var rows []table.Row + for _, repo := range repos { + + createdTime, _ := utils.FormatCreatedTime(repo.UpdateTime.String()) + rows = append(rows, table.Row{ + repo.Name, + fmt.Sprintf("%d", repo.ArtifactCount), + strconv.FormatInt(repo.PullCount, 10), + createdTime, + }) + } + + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(len(rows)), + ) + + // Set the styles for the table + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderBottom(true). + Bold(false) + + s.Selected = s.Selected. + Foreground(s.Cell.GetForeground()). + Background(s.Cell.GetBackground()). + Bold(false) + t.SetStyles(s) + + m := model{t} + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/pkg/views/repository/select/view.go b/pkg/views/repository/select/view.go new file mode 100644 index 00000000..6e5bed43 --- /dev/null +++ b/pkg/views/repository/select/view.go @@ -0,0 +1,121 @@ +package project + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" +) + +const listHeight = 25 + +var ( + titleStyle = lipgloss.NewStyle().MarginLeft(2) + itemStyle = lipgloss.NewStyle().PaddingLeft(4) + selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) + paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) + helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) +) + +type item string + +func (i item) FilterValue() string { return "" } + +type itemDelegate struct{} + +func (d itemDelegate) Height() int { return 1 } +func (d itemDelegate) Spacing() int { return 0 } +func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(item) + if !ok { + return + } + + str := fmt.Sprintf("%d. %s", index+1, i) + + fn := itemStyle.Render + if index == m.Index() { + fn = func(s ...string) string { + return selectedItemStyle.Render("> " + strings.Join(s, " ")) + } + } + + fmt.Fprint(w, fn(str)) +} + +type model struct { + list list.Model + choice string +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.list.SetWidth(msg.Width) + return m, nil + + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case "enter": + i, ok := m.list.SelectedItem().(item) + if ok { + m.choice = string(i) + } + return m, tea.Quit + } + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m model) View() string { + if m.choice != "" { + return "" + } + return "\n" + m.list.View() +} + +func RepositoryList(repos []*models.Repository, choice chan<- string) { + itemsList := make([]list.Item, len(repos)) + + for i, r := range repos { + split := strings.Split(r.Name, "/") + itemsList[i] = item(strings.Join(split[1:], "/")) + } + + const defaultWidth = 20 + + l := list.New(itemsList, itemDelegate{}, defaultWidth, listHeight) + l.Title = "Select a Repository" + l.SetShowStatusBar(false) + l.SetFilteringEnabled(false) + l.Styles.Title = titleStyle + l.Styles.PaginationStyle = paginationStyle + l.Styles.HelpStyle = helpStyle + + m := model{list: l} + + p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() + + if err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } + + if p, ok := p.(model); ok { + choice <- p.choice + } + +}