diff --git a/cmd/harbor/root/cmd.go b/cmd/harbor/root/cmd.go index 7a3fbf3d..bb70e322 100644 --- a/cmd/harbor/root/cmd.go +++ b/cmd/harbor/root/cmd.go @@ -6,6 +6,7 @@ import ( "os" "github.com/goharbor/harbor-cli/cmd/harbor/root/artifact" + "github.com/goharbor/harbor-cli/cmd/harbor/root/immutable" "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" @@ -109,6 +110,7 @@ harbor help repositry.Repository(), user.User(), artifact.Artifact(), + immutable.Immutable(), ) return root diff --git a/cmd/harbor/root/immutable/cmd.go b/cmd/harbor/root/immutable/cmd.go new file mode 100644 index 00000000..40d32199 --- /dev/null +++ b/cmd/harbor/root/immutable/cmd.go @@ -0,0 +1,21 @@ +package immutable + +import ( + "github.com/spf13/cobra" +) + +func Immutable() *cobra.Command { + cmd := &cobra.Command{ + Use: "immutable", + Short: "Manage Immutability rules in the project", + Long: `Manage Immutability rules in the project in Harbor`, + Example: `harbor immutable create`, + } + cmd.AddCommand( + CreateImmutableCommand(), + ListImmutableCommand(), + DeleteImmutableCommand(), + ) + + return cmd +} \ No newline at end of file diff --git a/cmd/harbor/root/immutable/create.go b/cmd/harbor/root/immutable/create.go new file mode 100644 index 00000000..3cf5ec24 --- /dev/null +++ b/cmd/harbor/root/immutable/create.go @@ -0,0 +1,62 @@ +package immutable + +import ( + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + "github.com/goharbor/harbor-cli/pkg/views/immutable/create" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func CreateImmutableCommand() *cobra.Command { + var opts create.CreateView + + cmd := &cobra.Command{ + Use: "create", + Short: "create immutable tag rule", + Long: "create immutable tag rule to the project in harbor", + Args: cobra.MaximumNArgs(1), + Example: "harbor immutable create", + Run: func(cmd *cobra.Command, args []string) { + var err error + createView := &create.CreateView{ + ScopeSelectors: create.ImmutableSelector{ + Decoration: opts.ScopeSelectors.Decoration, + Pattern: opts.ScopeSelectors.Pattern, + }, + TagSelectors: create.ImmutableSelector{ + Decoration: opts.TagSelectors.Decoration, + Pattern: opts.TagSelectors.Pattern, + }, + } + if len(args) > 0 { + err = createImmutableView(createView,args[0]) + } else { + projectName := prompt.GetProjectNameFromUser() + err = createImmutableView(createView,projectName) + } + + if err != nil { + log.Errorf("failed to create immutable tag rule: %v", err) + } + + }, + } + + flags := cmd.Flags() + flags.StringVarP(&opts.ScopeSelectors.Decoration, "repo-decoration", "", "", "repository which either apply or exclude from the rule") + flags.StringVarP(&opts.ScopeSelectors.Pattern, "repo-list", "", "", "list of repository to which to either apply or exclude from the rule") + flags.StringVarP(&opts.TagSelectors.Decoration, "tag-decoration", "", "", "tags which either apply or exclude from the rule") + flags.StringVarP(&opts.TagSelectors.Pattern, "tag-list", "", "", "list of tags to which to either apply or exclude from the rule") + + return cmd +} + +func createImmutableView(createView *create.CreateView,projectName string) error { + if createView == nil { + createView = &create.CreateView{} + } + + create.CreateImmutableView(createView) + return api.CreateImmutable(*createView,projectName) +} \ No newline at end of file diff --git a/cmd/harbor/root/immutable/delete.go b/cmd/harbor/root/immutable/delete.go new file mode 100644 index 00000000..c1d33b43 --- /dev/null +++ b/cmd/harbor/root/immutable/delete.go @@ -0,0 +1,33 @@ +package immutable + +import ( + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/spf13/cobra" + log "github.com/sirupsen/logrus" + "github.com/goharbor/harbor-cli/pkg/prompt" +) + +func DeleteImmutableCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "delete immutable rule", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + var immutableId int64 + if len(args) > 0 { + immutableId = prompt.GetImmutableTagRule(args[0]) + err = api.DeleteImmutable(args[0],immutableId) + } else { + projectName := prompt.GetProjectNameFromUser() + immutableId = prompt.GetImmutableTagRule(projectName) + err = api.DeleteImmutable(projectName,immutableId) + } + if err != nil { + log.Errorf("failed to delete immutable rule: %v", err) + } + }, + } + + return cmd +} \ No newline at end of file diff --git a/cmd/harbor/root/immutable/list.go b/cmd/harbor/root/immutable/list.go new file mode 100644 index 00000000..3bca6785 --- /dev/null +++ b/cmd/harbor/root/immutable/list.go @@ -0,0 +1,46 @@ +package immutable + +import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/immutable" + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/immutable/list" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func ListImmutableCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "list all immutable tag rule of project", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + var resp immutable.ListImmuRulesOK + + if len(args) > 0 { + projectName := args[0] + resp, err = api.ListImmutable(projectName) + } else { + projectName := prompt.GetProjectNameFromUser() + resp, err = api.ListImmutable(projectName) + } + + if err != nil { + log.Errorf("failed to list immutablility rule: %v", err) + } + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + utils.PrintPayloadInJSONFormat(resp) + return + } + + list.ListImmuRules(resp.Payload) + + }, + } + + return cmd +} \ No newline at end of file diff --git a/pkg/api/immutable_handler.go b/pkg/api/immutable_handler.go new file mode 100644 index 00000000..823fd924 --- /dev/null +++ b/pkg/api/immutable_handler.go @@ -0,0 +1,66 @@ +package api + +import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/immutable" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/immutable/create" + log "github.com/sirupsen/logrus" +) + +func CreateImmutable(opts create.CreateView, projectName string) error { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return err + } + tagSelector := &models.ImmutableSelector{ + Decoration: opts.TagSelectors.Decoration, + Pattern: opts.TagSelectors.Pattern, + } + scope := models.ImmutableSelector{ + Decoration: opts.ScopeSelectors.Decoration, + Pattern: opts.ScopeSelectors.Pattern, + } + scopeSelector := map[string][]models.ImmutableSelector{ + "repository": { + scope, + }, + } + + _, err = client.Immutable.CreateImmuRule(ctx, &immutable.CreateImmuRuleParams{ProjectNameOrID: projectName,ImmutableRule: &models.ImmutableRule{TagSelectors: []*models.ImmutableSelector{tagSelector},ScopeSelectors: scopeSelector}}) + + if err != nil { + return err + } + + log.Info("Added Tag Immutability Rule") + return nil +} + +func ListImmutable(projectName string)(immutable.ListImmuRulesOK,error){ + ctx, client, err := utils.ContextWithClient() + if err != nil { + return immutable.ListImmuRulesOK{}, err + } + response,err := client.Immutable.ListImmuRules(ctx,&immutable.ListImmuRulesParams{ProjectNameOrID: projectName}) + if err != nil { + return immutable.ListImmuRulesOK{}, err + } + + return *response, nil +} + +func DeleteImmutable(projectName string,ImmutableID int64) error{ + ctx, client, err := utils.ContextWithClient() + if err != nil { + return err + } + _, err = client.Immutable.DeleteImmuRule(ctx,&immutable.DeleteImmuRuleParams{ProjectNameOrID: projectName,ImmutableRuleID: ImmutableID}) + if err != nil { + return err + } + + log.Info("immutable rule deleted successfully") + + return nil +} \ No newline at end of file diff --git a/pkg/prompt/prompt.go b/pkg/prompt/prompt.go index f67f9b94..76223b01 100644 --- a/pkg/prompt/prompt.go +++ b/pkg/prompt/prompt.go @@ -8,6 +8,7 @@ import ( rview "github.com/goharbor/harbor-cli/pkg/views/registry/select" repoView "github.com/goharbor/harbor-cli/pkg/views/repository/select" uview "github.com/goharbor/harbor-cli/pkg/views/user/select" + iview "github.com/goharbor/harbor-cli/pkg/views/immutable/select" log "github.com/sirupsen/logrus" ) @@ -71,6 +72,15 @@ func GetUserIdFromUser() int64 { } +func GetImmutableTagRule(projectName string) int64 { + immutableid := make(chan int64) + go func() { + response, _ := api.ListImmutable(projectName) + iview.ImmutableList(response.Payload, immutableid) + }() + return <-immutableid +} + func GetTagFromUser(repoName, projectName, reference string) string { tag := make(chan string) go func() { diff --git a/pkg/views/immutable/create/view.go b/pkg/views/immutable/create/view.go new file mode 100644 index 00000000..17dcbd31 --- /dev/null +++ b/pkg/views/immutable/create/view.go @@ -0,0 +1,74 @@ +package create + +import ( + "errors" + + "github.com/charmbracelet/huh" + log "github.com/sirupsen/logrus" +) + +type CreateView struct { + ScopeSelectors ImmutableSelector `json:"scope_selectors,omitempty"` + TagSelectors ImmutableSelector `json:"tag_selectors"` +} + +type ImmutableSelector struct { + Decoration string `json:"decoration,omitempty"` + Pattern string `json:"pattern,omitempty"` +} + +func CreateImmutableView(createView *CreateView) { + theme := huh.ThemeCharm() + err := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("\nFor the repositories\n"). + Options( + huh.NewOption("matching", "repoMatches"), + huh.NewOption("excluding", "repoExcludes"), + ).Value(&createView.ScopeSelectors.Decoration). + Validate(func(str string) error { + if str == "" { + return errors.New("decoration cannot be empty") + } + return nil + }), + huh.NewInput(). + Title("List of repositories"). + Value(&createView.ScopeSelectors.Pattern). + Description("Enter multiple comma separated repos,repo*,or **"). + Validate(func(str string) error { + if str == "" { + return errors.New("pattern cannot be empty") + } + return nil + }), + huh.NewSelect[string](). + Title("Tags\n"). + Options( + huh.NewOption("matching", "matches"), + huh.NewOption("excluding", "excludes"), + ).Value(&createView.TagSelectors.Decoration). + Validate(func(str string) error { + if str == "" { + return errors.New("decoration cannot be empty") + } + return nil + }), + huh.NewInput(). + Title("List of Tags"). + Value(&createView.TagSelectors.Pattern). + Description("Enter multiple comma separated repos,repo*,or **"). + Validate(func(str string) error { + if str == "" { + return errors.New("pattern cannot be empty") + } + return nil + }), + ), + ).WithTheme(theme).Run() + + if err != nil { + log.Fatal(err) + } +} \ No newline at end of file diff --git a/pkg/views/immutable/list/view.go b/pkg/views/immutable/list/view.go new file mode 100644 index 00000000..db540422 --- /dev/null +++ b/pkg/views/immutable/list/view.go @@ -0,0 +1,48 @@ +package list + +import ( + "fmt" + "os" + "strings" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "ID", Width: 6}, + {Title: "Repository", Width: 26}, + {Title: "Tag", Width: 24}, +} + +func ListImmuRules(immutable []*models.ImmutableRule) { + var rows []table.Row + for _, regis := range immutable { + scopeSelectors := make([]string, len(regis.ScopeSelectors)) + for _, scope := range regis.ScopeSelectors { + for i, repo := range scope { + scopeSelectors[i] = fmt.Sprintf("%s %s", repo.Decoration, repo.Pattern) + } + } + + tagSelectors := make([]string, len(regis.TagSelectors)) + for i, tag := range regis.TagSelectors { + tagSelectors[i] = fmt.Sprintf("%s %s",tag.Decoration, tag.Pattern) + } + + rows = append(rows, table.Row{ + fmt.Sprintf("%d", regis.ID), + strings.Join(scopeSelectors, " "), + strings.Join(tagSelectors, " "), + }) + } + + m := tablelist.NewModel(columns, rows, len(rows)) + + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} \ No newline at end of file diff --git a/pkg/views/immutable/select/view.go b/pkg/views/immutable/select/view.go new file mode 100644 index 00000000..bdeb0534 --- /dev/null +++ b/pkg/views/immutable/select/view.go @@ -0,0 +1,50 @@ +package immutable + +import ( + "fmt" + "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/selection" +) + +func ImmutableList(immutablerule []*models.ImmutableRule, choice chan<- int64) { + itemsList := make([]list.Item, len(immutablerule)) + + items := map[string]int64{} + for i, r := range immutablerule { + scopeSelectors := make([]string, len(r.ScopeSelectors)) + tagSelectors := make([]string, len(r.TagSelectors)) + for _, scope := range r.ScopeSelectors { + for i, repo := range scope { + scopeSelectors[i] = fmt.Sprintf("%s %s",repo.Decoration, repo.Pattern) + } + } + for i, tag := range r.TagSelectors { + tagSelectors[i] = fmt.Sprintf("%s %s",tag.Decoration, tag.Pattern) + } + for _, scope := range scopeSelectors { + for _, tag := range tagSelectors { + immutablename := fmt.Sprintf("for the %s, tags %s", scope, tag) + items[immutablename] = r.ID + itemsList[i] = selection.Item(immutablename) + } + } + } + + m := selection.NewModel(itemsList, "Immutable Rule") + + 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] + } + +} \ No newline at end of file