diff --git a/cmd/harbor/root/cmd.go b/cmd/harbor/root/cmd.go index 630c75b52..b75bf926c 100644 --- a/cmd/harbor/root/cmd.go +++ b/cmd/harbor/root/cmd.go @@ -22,6 +22,7 @@ import ( "github.com/goharbor/harbor-cli/cmd/harbor/root/configurations" "github.com/goharbor/harbor-cli/cmd/harbor/root/context" "github.com/goharbor/harbor-cli/cmd/harbor/root/cve" + "github.com/goharbor/harbor-cli/cmd/harbor/root/gc" "github.com/goharbor/harbor-cli/cmd/harbor/root/instance" "github.com/goharbor/harbor-cli/cmd/harbor/root/jobservice" "github.com/goharbor/harbor-cli/cmd/harbor/root/labels" @@ -208,6 +209,10 @@ harbor help cmd.GroupID = "system" root.AddCommand(cmd) + cmd = gc.GC() + cmd.GroupID = "system" + root.AddCommand(cmd) + // Utils cmd = versionCommand() cmd.GroupID = "utils" diff --git a/cmd/harbor/root/gc/cmd.go b/cmd/harbor/root/gc/cmd.go new file mode 100644 index 000000000..f9af2b60a --- /dev/null +++ b/cmd/harbor/root/gc/cmd.go @@ -0,0 +1,50 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package gc + +import "github.com/spf13/cobra" + +func GC() *cobra.Command { + cmd := &cobra.Command{ + Use: "gc", + Short: "Manage Garbage Collection in Harbor", + Long: `Use this command to manage registry-wide Garbage Collection (GC) in your Harbor instance. + +Garbage Collection cleans up deleted or orphaned blobs/tags in the registry to free up storage space. +This command supports listing execution history, viewing logs, showing schedule configuration, stopping running jobs, and triggering manual runs.`, + Example: ` # View Garbage Collection execution history + harbor gc history + + # Get the current Garbage Collection schedule + harbor gc schedule + + # Trigger Garbage Collection run immediately + harbor gc trigger --delete-untagged --dry-run=false + + # View execution logs for a GC run + harbor gc log 12 + + # Stop a running Garbage Collection run + harbor gc stop 12 + + # Update the automatic Garbage Collection schedule + harbor gc update-schedule daily --delete-untagged`, + } + + cmd.AddCommand( + HistoryGCOperation(), + ) + + return cmd +} diff --git a/cmd/harbor/root/gc/gc_test.go b/cmd/harbor/root/gc/gc_test.go new file mode 100644 index 000000000..1288a1f36 --- /dev/null +++ b/cmd/harbor/root/gc/gc_test.go @@ -0,0 +1,53 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package gc + +import ( + "testing" + + "github.com/goharbor/harbor-cli/pkg/testutil" +) + +func TestHistoryGCOperation_Errors(t *testing.T) { + tests := []struct { + name string + flags []string + expectError bool + }{ + { + name: "negative page size", + flags: []string{"--page-size", "-1"}, + expectError: true, + }, + { + name: "page size too large", + flags: []string{"--page-size", "101"}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := testutil.TestCmd(t, HistoryGCOperation, tt.flags...) + + if tt.expectError && err == nil { + t.Fatalf("expected error but got nil") + } + + if !tt.expectError && err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} diff --git a/cmd/harbor/root/gc/history.go b/cmd/harbor/root/gc/history.go new file mode 100644 index 000000000..068486e7c --- /dev/null +++ b/cmd/harbor/root/gc/history.go @@ -0,0 +1,78 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package gc + +import ( + "fmt" + + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/gc/list" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func HistoryGCOperation() *cobra.Command { + var opts api.ListFlags + + cmd := &cobra.Command{ + Use: "history", + Short: "Get GC execution history", + Long: `Retrieve the execution history of registry-wide Garbage Collection jobs.`, + Example: ` harbor gc history --page 1 --page-size 10`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if opts.Page < 1 { + return fmt.Errorf("page number must be greater than or equal to 1") + } + if opts.PageSize < 0 { + return fmt.Errorf("page size must be greater than or equal to 0") + } + if opts.PageSize > 100 { + return fmt.Errorf("page size should be less than or equal to 100") + } + + logrus.Debug("Fetching Garbage Collection execution history") + history, err := api.ListGCHistory(opts) + if err != nil { + return fmt.Errorf("failed to list GC history: %v", utils.ParseHarborErrorMsg(err)) + } + + if len(history) == 0 { + fmt.Println("No Garbage Collection execution history found") + return nil + } + + formatFlag := viper.GetString("output-format") + if formatFlag != "" { + err = utils.PrintFormat(history, formatFlag) + if err != nil { + return err + } + } else { + list.ListGCHistory(history) + } + return nil + }, + } + + flags := cmd.Flags() + flags.Int64VarP(&opts.Page, "page", "", 1, "Page number") + flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page") + flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources") + flags.StringVarP(&opts.Sort, "sort", "", "", "Sort the resource list in ascending or descending order") + + return cmd +} diff --git a/doc/cli-docs/harbor-gc-history.md b/doc/cli-docs/harbor-gc-history.md new file mode 100644 index 000000000..0a0b73faf --- /dev/null +++ b/doc/cli-docs/harbor-gc-history.md @@ -0,0 +1,46 @@ +--- +title: harbor gc history +weight: 80 +--- +## harbor gc history + +### Description + +##### Get GC execution history + +### Synopsis + +Retrieve the execution history of registry-wide Garbage Collection jobs. + +```sh +harbor gc history [flags] +``` + +### Examples + +```sh + harbor gc history --page 1 --page-size 10 +``` + +### Options + +```sh + -h, --help help for history + --page int Page number (default 1) + --page-size int Size of per page (default 10) + -q, --query string Query string to query resources + --sort string Sort the resource list in ascending or descending order +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml|csv + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor gc](harbor-gc.md) - Manage Garbage Collection in Harbor + diff --git a/doc/cli-docs/harbor-gc.md b/doc/cli-docs/harbor-gc.md new file mode 100644 index 000000000..e81176f40 --- /dev/null +++ b/doc/cli-docs/harbor-gc.md @@ -0,0 +1,58 @@ +--- +title: harbor gc +weight: 15 +--- +## harbor gc + +### Description + +##### Manage Garbage Collection in Harbor + +### Synopsis + +Use this command to manage registry-wide Garbage Collection (GC) in your Harbor instance. + +Garbage Collection cleans up deleted or orphaned blobs/tags in the registry to free up storage space. +This command supports listing execution history, viewing logs, showing schedule configuration, stopping running jobs, and triggering manual runs. + +### Examples + +```sh + # View Garbage Collection execution history + harbor gc history + + # Get the current Garbage Collection schedule + harbor gc schedule + + # Trigger Garbage Collection run immediately + harbor gc trigger --delete-untagged --dry-run=false + + # View execution logs for a GC run + harbor gc log 12 + + # Stop a running Garbage Collection run + harbor gc stop 12 + + # Update the automatic Garbage Collection schedule + harbor gc update-schedule daily --delete-untagged +``` + +### Options + +```sh + -h, --help help for gc +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml|csv + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor](harbor.md) - Official Harbor CLI +* [harbor gc history](harbor-gc-history.md) - Get GC execution history + diff --git a/doc/cli-docs/harbor.md b/doc/cli-docs/harbor.md index ec7dd3749..6361772b6 100644 --- a/doc/cli-docs/harbor.md +++ b/doc/cli-docs/harbor.md @@ -39,6 +39,7 @@ harbor help * [harbor config](harbor-config.md) - Manage system configurations * [harbor context](harbor-context.md) - Manage locally available contexts * [harbor cve-allowlist](harbor-cve-allowlist.md) - Manage system CVE allowlist +* [harbor gc](harbor-gc.md) - Manage Garbage Collection in Harbor * [harbor health](harbor-health.md) - Get the health status of Harbor components * [harbor info](harbor-info.md) - Display detailed Harbor system, statistics, and CLI environment information * [harbor instance](harbor-instance.md) - Manage preheat provider instances in Harbor diff --git a/doc/man-docs/man1/harbor-gc-history.1 b/doc/man-docs/man1/harbor-gc-history.1 new file mode 100644 index 000000000..3fcae887a --- /dev/null +++ b/doc/man-docs/man1/harbor-gc-history.1 @@ -0,0 +1,57 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-gc-history - Get GC execution history + + +.SH SYNOPSIS +\fBharbor gc history [flags]\fP + + +.SH DESCRIPTION +Retrieve the execution history of registry-wide Garbage Collection jobs. + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for history + +.PP +\fB--page\fP=1 + Page number + +.PP +\fB--page-size\fP=10 + Size of per page + +.PP +\fB-q\fP, \fB--query\fP="" + Query string to query resources + +.PP +\fB--sort\fP="" + Sort the resource list in ascending or descending order + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB-c\fP, \fB--config\fP="" + config file (default is $HOME/.config/harbor-cli/config.yaml) + +.PP +\fB-o\fP, \fB--output-format\fP="" + Output format. One of: json|yaml|csv + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX + harbor gc history --page 1 --page-size 10 +.EE + + +.SH SEE ALSO +\fBharbor-gc(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-gc.1 b/doc/man-docs/man1/harbor-gc.1 new file mode 100644 index 000000000..3fdfe6f16 --- /dev/null +++ b/doc/man-docs/man1/harbor-gc.1 @@ -0,0 +1,61 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-gc - Manage Garbage Collection in Harbor + + +.SH SYNOPSIS +\fBharbor gc [flags]\fP + + +.SH DESCRIPTION +Use this command to manage registry-wide Garbage Collection (GC) in your Harbor instance. + +.PP +Garbage Collection cleans up deleted or orphaned blobs/tags in the registry to free up storage space. +This command supports listing execution history, viewing logs, showing schedule configuration, stopping running jobs, and triggering manual runs. + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for gc + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB-c\fP, \fB--config\fP="" + config file (default is $HOME/.config/harbor-cli/config.yaml) + +.PP +\fB-o\fP, \fB--output-format\fP="" + Output format. One of: json|yaml|csv + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX + # View Garbage Collection execution history + harbor gc history + + # Get the current Garbage Collection schedule + harbor gc schedule + + # Trigger Garbage Collection run immediately + harbor gc trigger --delete-untagged --dry-run=false + + # View execution logs for a GC run + harbor gc log 12 + + # Stop a running Garbage Collection run + harbor gc stop 12 + + # Update the automatic Garbage Collection schedule + harbor gc update-schedule daily --delete-untagged +.EE + + +.SH SEE ALSO +\fBharbor(1)\fP, \fBharbor-gc-history(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor.1 b/doc/man-docs/man1/harbor.1 index 741d8f706..147652c56 100644 --- a/doc/man-docs/man1/harbor.1 +++ b/doc/man-docs/man1/harbor.1 @@ -43,4 +43,4 @@ harbor help .SH SEE ALSO -\fBharbor-artifact(1)\fP, \fBharbor-config(1)\fP, \fBharbor-context(1)\fP, \fBharbor-cve-allowlist(1)\fP, \fBharbor-health(1)\fP, \fBharbor-info(1)\fP, \fBharbor-instance(1)\fP, \fBharbor-jobservice(1)\fP, \fBharbor-label(1)\fP, \fBharbor-ldap(1)\fP, \fBharbor-login(1)\fP, \fBharbor-logs(1)\fP, \fBharbor-password(1)\fP, \fBharbor-project(1)\fP, \fBharbor-quota(1)\fP, \fBharbor-registry(1)\fP, \fBharbor-replication(1)\fP, \fBharbor-repo(1)\fP, \fBharbor-robot(1)\fP, \fBharbor-scan-all(1)\fP, \fBharbor-scanner(1)\fP, \fBharbor-schedule(1)\fP, \fBharbor-tag(1)\fP, \fBharbor-user(1)\fP, \fBharbor-version(1)\fP, \fBharbor-vulnerability(1)\fP, \fBharbor-webhook(1)\fP \ No newline at end of file +\fBharbor-artifact(1)\fP, \fBharbor-config(1)\fP, \fBharbor-context(1)\fP, \fBharbor-cve-allowlist(1)\fP, \fBharbor-gc(1)\fP, \fBharbor-health(1)\fP, \fBharbor-info(1)\fP, \fBharbor-instance(1)\fP, \fBharbor-jobservice(1)\fP, \fBharbor-label(1)\fP, \fBharbor-ldap(1)\fP, \fBharbor-login(1)\fP, \fBharbor-logs(1)\fP, \fBharbor-password(1)\fP, \fBharbor-project(1)\fP, \fBharbor-quota(1)\fP, \fBharbor-registry(1)\fP, \fBharbor-replication(1)\fP, \fBharbor-repo(1)\fP, \fBharbor-robot(1)\fP, \fBharbor-scan-all(1)\fP, \fBharbor-scanner(1)\fP, \fBharbor-schedule(1)\fP, \fBharbor-tag(1)\fP, \fBharbor-user(1)\fP, \fBharbor-version(1)\fP, \fBharbor-vulnerability(1)\fP, \fBharbor-webhook(1)\fP \ No newline at end of file diff --git a/pkg/api/gc_handler.go b/pkg/api/gc_handler.go new file mode 100644 index 000000000..014ec660f --- /dev/null +++ b/pkg/api/gc_handler.go @@ -0,0 +1,39 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package api + +import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/gc" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" +) + +func ListGCHistory(opts ListFlags) ([]*models.GCHistory, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + + response, err := client.GC.GetGCHistory(ctx, &gc.GetGCHistoryParams{ + Page: &opts.Page, + PageSize: &opts.PageSize, + Q: &opts.Q, + Sort: &opts.Sort, + }) + if err != nil { + return nil, err + } + + return response.Payload, nil +} diff --git a/pkg/views/gc/list/view.go b/pkg/views/gc/list/view.go new file mode 100644 index 000000000..a76641b9b --- /dev/null +++ b/pkg/views/gc/list/view.go @@ -0,0 +1,78 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package list + +import ( + "encoding/json" + "fmt" + "os" + "sort" + "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/utils" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "ID", Width: tablelist.WidthXS}, + {Title: "Job Name", Width: tablelist.WidthL}, + {Title: "Status", Width: tablelist.WidthM}, + {Title: "Kind", Width: tablelist.WidthM}, + {Title: "Parameters", Width: tablelist.WidthXL}, + {Title: "Creation Time", Width: tablelist.WidthXL}, +} + +func ListGCHistory(history []*models.GCHistory) { + var rows []table.Row + for _, run := range history { + createdTime, _ := utils.FormatCreatedTime(run.CreationTime.String()) + rows = append(rows, table.Row{ + fmt.Sprintf("%d", run.ID), + run.JobName, + run.JobStatus, + run.JobKind, + formatParams(run.JobParameters), + 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) + } +} + +func formatParams(paramsStr string) string { + if paramsStr == "" { + return "-" + } + var m map[string]interface{} + if err := json.Unmarshal([]byte(paramsStr), &m); err != nil { + return paramsStr + } + var parts []string + for k, v := range m { + parts = append(parts, fmt.Sprintf("%s=%v", k, v)) + } + if len(parts) == 0 { + return "-" + } + sort.Strings(parts) + return strings.Join(parts, ", ") +}