diff --git a/cmd/harbor/root/cmd.go b/cmd/harbor/root/cmd.go index 7a3fbf3d..66f738cb 100644 --- a/cmd/harbor/root/cmd.go +++ b/cmd/harbor/root/cmd.go @@ -9,6 +9,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/cmd/harbor/root/retention" "github.com/goharbor/harbor-cli/cmd/harbor/root/user" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/spf13/cobra" @@ -109,6 +110,7 @@ harbor help repositry.Repository(), user.User(), artifact.Artifact(), + retention.Retention(), ) return root diff --git a/cmd/harbor/root/retention/cmd.go b/cmd/harbor/root/retention/cmd.go new file mode 100644 index 00000000..1c8ecdfb --- /dev/null +++ b/cmd/harbor/root/retention/cmd.go @@ -0,0 +1,21 @@ +package retention + +import ( + "github.com/spf13/cobra" +) + +func Retention() *cobra.Command { + cmd := &cobra.Command{ + Use: "retention", + Short: "Manage retention rule in the project", + Long: `Manage retention rules in the project in Harbor`, + Example: `harbor retention create`, + } + cmd.AddCommand( + CreateRetentionCommand(), + ListExecutionRetentionCommand(), + DeleteRetentionCommand(), + ) + + return cmd +} \ No newline at end of file diff --git a/cmd/harbor/root/retention/create.go b/cmd/harbor/root/retention/create.go new file mode 100644 index 00000000..756713e3 --- /dev/null +++ b/cmd/harbor/root/retention/create.go @@ -0,0 +1,70 @@ +package retention + +import ( + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + "github.com/goharbor/harbor-cli/pkg/views/retention/create" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func CreateRetentionCommand() *cobra.Command { + var opts create.CreateView + + cmd := &cobra.Command{ + Use: "create", + Short: "create retention tag rule", + Long: "create retention tag rule to the project in harbor", + Example: "harbor retention create", + Run: func(cmd *cobra.Command, args []string) { + var err error + createView := &create.CreateView{ + ScopeSelectors: create.RetentionSelector{ + Decoration: opts.ScopeSelectors.Decoration, + Pattern: opts.ScopeSelectors.Pattern, + }, + TagSelectors: create.RetentionSelector{ + Decoration: opts.TagSelectors.Decoration, + Pattern: opts.TagSelectors.Pattern, + Extras: opts.TagSelectors.Extras, + }, + Scope: create.RetentionPolicyScope{ + Level: opts.Scope.Level, + Ref: opts.Scope.Ref, + }, + Template: opts.Template, + Params: opts.Params, + Action: opts.Action, + Algorithm: opts.Algorithm, + } + + projectId := int32(prompt.GetProjectIDFromUser()) + err = createRetentionView(createView,projectId) + + if err != nil { + log.Errorf("failed to create retention tag rule: %v", err) + } + + }, + } + + flags := cmd.Flags() + flags.StringVarP(&opts.ScopeSelectors.Decoration, "repodecoration", "", "", "repository which either apply or exclude from the rule") + flags.StringVarP(&opts.ScopeSelectors.Pattern, "repolist", "", "", "list of repository to which to either apply or exclude from the rule") + flags.StringVarP(&opts.TagSelectors.Decoration, "tagdecoration", "", "", "tags which either apply or exclude from the rule") + flags.StringVarP(&opts.TagSelectors.Pattern, "taglist", "", "", "list of tags to which to either apply or exclude from the rule") + flags.StringVarP(&opts.Scope.Level,"level","","project","scope of retention policy") + flags.StringVarP(&opts.Action,"action","","retain","Action of the retention policy") + flags.StringVarP(&opts.Algorithm,"algorithm","","or","Algorithm of retention policy") + + return cmd +} + +func createRetentionView(createView *create.CreateView,projectId int32) error { + if createView == nil { + createView = &create.CreateView{} + } + + create.CreateRetentionView(createView) + return api.CreateRetention(*createView,projectId) +} \ No newline at end of file diff --git a/cmd/harbor/root/retention/delete.go b/cmd/harbor/root/retention/delete.go new file mode 100644 index 00000000..d8796c65 --- /dev/null +++ b/cmd/harbor/root/retention/delete.go @@ -0,0 +1,42 @@ +package retention + +import ( + "fmt" + "strconv" + + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func DeleteRetentionCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "delete retention rule", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + var retentionId int + var strretenId string + if len(args) > 0 { + retentionId,_ = strconv.Atoi(args[0]) + err = api.DeleteRetention(int64(retentionId)) + } else { + projectId := fmt.Sprintf("%d",prompt.GetProjectIDFromUser()) + strretenId,err = api.GetRetentionId(projectId) + if err != nil { + log.Fatal(err) + } + retentionId,_ = strconv.Atoi(strretenId) + err = api.DeleteRetention(int64(retentionId)) + } + if err != nil { + log.Errorf("failed to delete retention rule: %v", err) + } + }, + } + + return cmd +} + diff --git a/cmd/harbor/root/retention/list.go b/cmd/harbor/root/retention/list.go new file mode 100644 index 00000000..e42b376e --- /dev/null +++ b/cmd/harbor/root/retention/list.go @@ -0,0 +1,56 @@ +package retention + +import ( + "fmt" + "strconv" + + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/retention" + "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/retention/list" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func ListExecutionRetentionCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "list retention execution of the project", + Args: cobra.MaximumNArgs(1), + Example: `harbor retention list [retentionid]`, + Run: func(cmd *cobra.Command, args []string) { + var err error + var resp retention.ListRetentionExecutionsOK + var retentionID int + var strretenId string + if len(args) > 0 { + retentionID,_ = strconv.Atoi(args[0]) + resp, err = api.ListRetention(int32(retentionID)) + } else { + projectId := fmt.Sprintf("%d",prompt.GetProjectIDFromUser()) + strretenId,err = api.GetRetentionId(projectId) + if err != nil { + log.Fatal(err) + } + retentionID,_ := strconv.Atoi(strretenId) + resp, err = api.ListRetention(int32(retentionID)) + } + + if err != nil { + log.Errorf("failed to list retention execution: %v", err) + } + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + utils.PrintPayloadInJSONFormat(resp) + return + } + + list.ListRetentionRules(resp.Payload) + + }, + } + + return cmd +} \ No newline at end of file diff --git a/pkg/api/retention_handler.go b/pkg/api/retention_handler.go new file mode 100644 index 00000000..caca0854 --- /dev/null +++ b/pkg/api/retention_handler.go @@ -0,0 +1,114 @@ +package api + +import ( + "errors" + "strconv" + + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/project" + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/retention" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/retention/create" + log "github.com/sirupsen/logrus" +) + +func CreateRetention(opts create.CreateView, projectId int32) error { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return err + } + + tagSelector := &models.RetentionSelector{ + Decoration: opts.TagSelectors.Decoration, + Pattern: opts.TagSelectors.Pattern, + Extras: opts.TagSelectors.Extras, + } + scope := models.RetentionSelector{ + Decoration: opts.ScopeSelectors.Decoration, + Pattern: opts.ScopeSelectors.Pattern, + } + scopeSelector := map[string][]models.RetentionSelector{ + "repository": { + scope, + }, + } + param := make(map[string] interface{}) + if opts.Template == "always" { + param = nil + } else { + value, err := strconv.Atoi(opts.Params.Value) + if err != nil { + return err + } + param[opts.Template] = value + } + + var rule []*models.RetentionRule + rule = append(rule, &models.RetentionRule{ + Action: opts.Action, + ScopeSelectors: scopeSelector, + TagSelectors: []*models.RetentionSelector{tagSelector}, + Template: opts.Template, + Params: param, + }) + + triggerSettings := map[string]string{ + "cron": "", + } + + _, err = client.Retention.CreateRetention(ctx, &retention.CreateRetentionParams{Policy: &models.RetentionPolicy{Scope: &models.RetentionPolicyScope{Level: opts.Scope.Level,Ref: int64(projectId)},Trigger: &models.RetentionRuleTrigger{Kind: models.ScheduleObjTypeSchedule,Settings: triggerSettings},Algorithm: opts.Algorithm,Rules: rule}}) + if err != nil { + return err + } + + log.Info("Added Tag Retention Rule") + return nil +} + +func ListRetention(projectID int32)(retention.ListRetentionExecutionsOK,error){ + ctx, client, err := utils.ContextWithClient() + if err != nil { + return retention.ListRetentionExecutionsOK{}, err + } + response,err := client.Retention.ListRetentionExecutions(ctx,&retention.ListRetentionExecutionsParams{ID: int64(projectID)}) + if err != nil { + return retention.ListRetentionExecutionsOK{}, err + } + + return *response, nil +} + +func GetRetentionId(projectId string) (string,error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return "",err + } + + response, err := client.Project.GetProject(ctx, &project.GetProjectParams{ProjectNameOrID: projectId}) + if err != nil { + log.Errorf("failed to get project: %v", err) + return "", err + } + + if response.Payload.Metadata == nil || response.Payload.Metadata.RetentionID == nil { + return "", errors.New("no retention policy present for the project") + } + retentionid := *response.Payload.Metadata.RetentionID + + return retentionid,nil +} + +func DeleteRetention(RetentionID int64) error{ + ctx, client, err := utils.ContextWithClient() + if err != nil { + return err + } + _, err = client.Retention.DeleteRetention(ctx,&retention.DeleteRetentionParams{ID: RetentionID}) + if err != nil { + return err + } + + log.Info("retention 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..5a27714b 100644 --- a/pkg/prompt/prompt.go +++ b/pkg/prompt/prompt.go @@ -34,6 +34,17 @@ func GetProjectNameFromUser() string { return <-projectName } +func GetProjectIDFromUser() int32 { + projectId := make(chan int32) + go func() { + response, _ := api.ListProject() + pview.ProjectIdList(response.Payload, projectId) + + }() + + return <-projectId +} + func GetRepoNameFromUser(projectName string) string { repositoryName := make(chan string) diff --git a/pkg/views/project/select/view.go b/pkg/views/project/select/view.go index a3fad35c..91a278c2 100644 --- a/pkg/views/project/select/view.go +++ b/pkg/views/project/select/view.go @@ -30,3 +30,26 @@ func ProjectList(project []*models.Project, choice chan<- string) { } } + +func ProjectIdList(project []*models.Project, choice chan<- int32) { + itemsList := make([]list.Item, len(project)) + items := map[string]int32{} + for i, p := range project { + items[p.Name] = p.ProjectID + itemsList[i] = selection.Item(p.Name) + } + + m := selection.NewModel(itemsList, "Project") + + 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/retention/create/view.go b/pkg/views/retention/create/view.go new file mode 100644 index 00000000..cf3eb99b --- /dev/null +++ b/pkg/views/retention/create/view.go @@ -0,0 +1,147 @@ +package create + +import ( + "errors" + + "github.com/charmbracelet/huh" + log "github.com/sirupsen/logrus" +) + +type CreateView struct { + Action string + ScopeSelectors RetentionSelector + TagSelectors RetentionSelector + Template string + Algorithm string + Params ParamsValue + Scope RetentionPolicyScope +} + +type RetentionSelector struct { + Decoration string + Pattern string + Extras string + Kind string +} + +type ParamsValue struct { + Name string + Value string +} + +type RetentionPolicyScope struct { + Level string + Ref int64 +} + +func CreateRetentionView(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 tags, tag*, or **."). + Validate(func(str string) error { + if str == "" { + return errors.New("pattern cannot be empty") + } + return nil + }), + huh.NewSelect[string](). + Title("Untagged Artifacts\n"). + Description("Include or exclude all untagged artifacts by selecting true or false"). + Options( + huh.NewOption("true", "{\"untagged\":true}"), + huh.NewOption("false", "{\"untagged\":false}"), + ).Value(&createView.TagSelectors.Extras). + Validate(func(str string) error { + if str == "" { + return errors.New("this field cannot be empty") + } + return nil + }), + huh.NewSelect[string](). + Title("\nSelect the condition of retain\n"). + Options( + huh.NewOption("retain the most recently pushed # artifacts", "latestPushedK"), + huh.NewOption("retain the most recently pulled # artifacts", "latestPulledN"), + huh.NewOption("retain the artifacts pushed within the last # days", "nDaysSinceLastPush"), + huh.NewOption("retain the artifacts pulled within the last # days", "nDaysSinceLastPull"), + huh.NewOption("retain always", "always"), + ).Value(&createView.Template). + Validate(func(str string) error { + if str == "" { + return errors.New("this field cannot be empty") + } + return nil + }), + ), + huh.NewGroup( + huh.NewInput(). + Title("Count"). + Value(&createView.Params.Value). + Description("Enter the number of artifact count"). + Validate(func(str string) error { + if str == "" { + return errors.New("count cannot be empty") + } + return nil + }), + ).WithHideFunc(func() bool { + return createView.Template == "always" || createView.Template == "nDaysSinceLastPush" || createView.Template == "nDaysSinceLastPull" + }), + huh.NewGroup( + huh.NewInput(). + Title("Days"). + Value(&createView.Params.Value). + Description("Enter the number of days"). + Validate(func(str string) error { + if str == "" { + return errors.New("days cannot be empty") + } + return nil + }), + ).WithHideFunc(func() bool { + return createView.Template == "always" || createView.Template == "latestPulledN" || createView.Template == "latestPushedK" + }), + ).WithTheme(theme).Run() + + if err != nil { + log.Fatal(err) + } +} \ No newline at end of file diff --git a/pkg/views/retention/list/view.go b/pkg/views/retention/list/view.go new file mode 100644 index 00000000..4ad75326 --- /dev/null +++ b/pkg/views/retention/list/view.go @@ -0,0 +1,41 @@ +package list + +import ( + "fmt" + "os" + + "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/utils" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "ID", Width: 6}, + {Title: "Status", Width: 10}, + {Title: "Dry Run", Width: 10}, + {Title: "Execution Type", Width: 16}, + {Title: "Start Time", Width: 18}, +} + +func ListRetentionRules(retention []*models.RetentionExecution) { + var rows []table.Row + for _, regis := range retention { + createdTime, _ := utils.FormatCreatedTime(regis.StartTime) + rows = append(rows, table.Row{ + fmt.Sprintf("%d", regis.ID), + regis.Status, + fmt.Sprintf("%v", regis.DryRun), + regis.Trigger, + createdTime, + }) + } + + 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