From 2a1024994d9490e56cc2726b58d939f273c078a3 Mon Sep 17 00:00:00 2001 From: Nishchay Rajput Date: Fri, 3 Apr 2026 13:03:15 +0530 Subject: [PATCH 1/3] feat: jobservice schedule Signed-off-by: Nishchay Rajput --- cmd/harbor/root/cmd.go | 5 + cmd/harbor/root/jobservice/cmd.go | 37 ++++ cmd/harbor/root/jobservice/schedules/list.go | 189 ++++++++++++++++++ .../root/jobservice/schedules/list_test.go | 94 +++++++++ .../harbor-jobservice-schedules-list.md | 44 ++++ .../harbor-jobservice-schedules-pause-all.md | 42 ++++ .../harbor-jobservice-schedules-resume-all.md | 42 ++++ .../harbor-jobservice-schedules-status.md | 42 ++++ doc/cli-docs/harbor-jobservice-schedules.md | 36 ++++ doc/cli-docs/harbor-jobservice.md | 36 ++++ doc/cli-docs/harbor.md | 1 + .../man1/harbor-jobservice-schedules-list.1 | 49 +++++ .../harbor-jobservice-schedules-pause-all.1 | 41 ++++ .../harbor-jobservice-schedules-resume-all.1 | 41 ++++ .../man1/harbor-jobservice-schedules-status.1 | 41 ++++ .../man1/harbor-jobservice-schedules.1 | 35 ++++ doc/man-docs/man1/harbor-jobservice.1 | 39 ++++ doc/man-docs/man1/harbor.1 | 2 +- pkg/api/jobservice_handler.go | 73 +++++++ pkg/views/jobservice/schedules/view.go | 55 +++++ pkg/views/jobservice/schedules/view_test.go | 117 +++++++++++ 21 files changed, 1060 insertions(+), 1 deletion(-) create mode 100644 cmd/harbor/root/jobservice/cmd.go create mode 100644 cmd/harbor/root/jobservice/schedules/list.go create mode 100644 cmd/harbor/root/jobservice/schedules/list_test.go create mode 100644 doc/cli-docs/harbor-jobservice-schedules-list.md create mode 100644 doc/cli-docs/harbor-jobservice-schedules-pause-all.md create mode 100644 doc/cli-docs/harbor-jobservice-schedules-resume-all.md create mode 100644 doc/cli-docs/harbor-jobservice-schedules-status.md create mode 100644 doc/cli-docs/harbor-jobservice-schedules.md create mode 100644 doc/cli-docs/harbor-jobservice.md create mode 100644 doc/man-docs/man1/harbor-jobservice-schedules-list.1 create mode 100644 doc/man-docs/man1/harbor-jobservice-schedules-pause-all.1 create mode 100644 doc/man-docs/man1/harbor-jobservice-schedules-resume-all.1 create mode 100644 doc/man-docs/man1/harbor-jobservice-schedules-status.1 create mode 100644 doc/man-docs/man1/harbor-jobservice-schedules.1 create mode 100644 doc/man-docs/man1/harbor-jobservice.1 create mode 100644 pkg/api/jobservice_handler.go create mode 100644 pkg/views/jobservice/schedules/view.go create mode 100644 pkg/views/jobservice/schedules/view_test.go diff --git a/cmd/harbor/root/cmd.go b/cmd/harbor/root/cmd.go index 33509cbf8..48690b675 100644 --- a/cmd/harbor/root/cmd.go +++ b/cmd/harbor/root/cmd.go @@ -23,6 +23,7 @@ import ( "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/instance" + "github.com/goharbor/harbor-cli/cmd/harbor/root/jobservice" "github.com/goharbor/harbor-cli/cmd/harbor/root/labels" "github.com/goharbor/harbor-cli/cmd/harbor/root/ldap" "github.com/goharbor/harbor-cli/cmd/harbor/root/project" @@ -203,6 +204,10 @@ harbor help cmd.GroupID = "system" root.AddCommand(cmd) + cmd = jobservice.JobService() + cmd.GroupID = "system" + root.AddCommand(cmd) + // Utils cmd = versionCommand() cmd.GroupID = "utils" diff --git a/cmd/harbor/root/jobservice/cmd.go b/cmd/harbor/root/jobservice/cmd.go new file mode 100644 index 000000000..045fb9773 --- /dev/null +++ b/cmd/harbor/root/jobservice/cmd.go @@ -0,0 +1,37 @@ +// 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 jobservice + +import ( + "github.com/goharbor/harbor-cli/cmd/harbor/root/jobservice/schedules" + "github.com/spf13/cobra" +) + +// JobService creates the jobservice command +func JobService() *cobra.Command { + cmd := &cobra.Command{ + Use: "jobservice", + Short: "Manage Harbor job service (admin only)", + Long: `Manage Harbor job service components including worker pools, job queues, schedules, and job logs. +This requires system admin privileges. + +Use "harbor jobservice [command] --help" for detailed examples and flags per subcommand.`, + } + + cmd.AddCommand( + schedules.SchedulesCommand(), + ) + + return cmd +} diff --git a/cmd/harbor/root/jobservice/schedules/list.go b/cmd/harbor/root/jobservice/schedules/list.go new file mode 100644 index 000000000..88b4f931d --- /dev/null +++ b/cmd/harbor/root/jobservice/schedules/list.go @@ -0,0 +1,189 @@ +// 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 schedules + +import ( + "fmt" + + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/jobservice/schedules" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// SchedulesCommand creates the schedules subcommand +func SchedulesCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "schedules", + Short: "Manage schedules (list/status/pause-all/resume-all)", + Long: "List schedules and manage global scheduler status.", + } + + cmd.AddCommand( + ListCommand(), + StatusCommand(), + PauseAllCommand(), + ResumeAllCommand(), + ) + + return cmd +} + +// ListCommand lists all schedules +func ListCommand() *cobra.Command { + var page int64 = 1 + var pageSize int64 = 20 + + cmd := &cobra.Command{ + Use: "list", + Short: "List schedules (supports --page and --page-size)", + Long: "Display all job schedules with pagination support.", + Example: "harbor jobservice schedules list --page 1 --page-size 20", + RunE: func(cmd *cobra.Command, args []string) error { + if page < 1 { + return fmt.Errorf("page must be >= 1") + } + if pageSize < 1 || pageSize > 100 { + return fmt.Errorf("page-size must be between 1 and 100") + } + + response, err := api.ListSchedules(page, pageSize) + if err != nil { + return formatScheduleError("failed to retrieve schedules", err, "ActionList") + } + + if response == nil || response.Payload == nil || len(response.Payload) == 0 { + fmt.Println("No schedules found.") + return nil + } + + formatFlag := viper.GetString("output-format") + if formatFlag != "" { + return utils.PrintFormat(response.Payload, formatFlag) + } + + totalCount := response.XTotalCount + schedules.ListSchedules(response.Payload, page, pageSize, totalCount) + return nil + }, + } + + flags := cmd.Flags() + flags.Int64Var(&page, "page", 1, "Page number") + flags.Int64Var(&pageSize, "page-size", 20, "Number of items per page") + + return cmd +} + +// StatusCommand shows the global scheduler status +func StatusCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "status", + Short: "Show scheduler status", + Long: "Display whether the global scheduler is paused or running.", + Example: "harbor jobservice schedules status", + RunE: func(cmd *cobra.Command, args []string) error { + response, err := api.GetSchedulePaused() + if err != nil { + return formatScheduleError("failed to retrieve scheduler status", err, "authenticated") + } + + if response == nil || response.Payload == nil { + fmt.Println("Unable to determine scheduler status.") + return nil + } + + formatFlag := viper.GetString("output-format") + if formatFlag != "" { + return utils.PrintFormat(response.Payload, formatFlag) + } + + schedules.PrintScheduleStatus(response.Payload) + return nil + }, + } + + return cmd +} + +// PauseAllCommand pauses all schedules +func PauseAllCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "pause-all", + Short: "Pause all schedules", + Long: "Pause the global scheduler and all schedules.", + Example: "harbor jobservice schedules pause-all", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("Pausing all schedules...") + err := api.ActionJobQueue("SCHEDULER", "pause") + if err != nil { + return formatScheduleError("failed to pause all schedules", err, "ActionStop") + } + fmt.Println("✓ All schedules paused successfully.") + return nil + }, + } + + return cmd +} + +// ResumeAllCommand resumes all schedules +func ResumeAllCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "resume-all", + Short: "Resume all schedules", + Long: "Resume the global scheduler and all schedules.", + Example: "harbor jobservice schedules resume-all", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("Resuming all schedules...") + err := api.ActionJobQueue("SCHEDULER", "resume") + if err != nil { + return formatScheduleError("failed to resume all schedules", err, "ActionStop") + } + fmt.Println("✓ All schedules resumed successfully.") + return nil + }, + } + + return cmd +} + +func formatScheduleError(operation string, err error, requiredPermission string) error { + errorCode := utils.ParseHarborErrorCode(err) + + switch errorCode { + case "400": + return fmt.Errorf("%s: invalid request. For schedule status use job_type=all; for queue action use stop|pause|resume", operation) + case "401": + return fmt.Errorf("%s: authentication required. Please run 'harbor login' and try again", operation) + case "403": + if requiredPermission == "authenticated" { + return fmt.Errorf("%s: permission denied. Your account is authenticated but lacks access", operation) + } + return fmt.Errorf("%s: permission denied. This operation requires %s on jobservice-monitor", operation, requiredPermission) + case "404": + return fmt.Errorf("%s: resource not found or not accessible in current context", operation) + case "422": + return fmt.Errorf("%s: request validation failed. Please check request body and action values", operation) + case "500": + return fmt.Errorf("%s: Harbor internal error. Retry and check Harbor server logs", operation) + default: + msg := utils.ParseHarborErrorMsg(err) + if msg == "" { + msg = err.Error() + } + return fmt.Errorf("%s: %s", operation, msg) + } +} diff --git a/cmd/harbor/root/jobservice/schedules/list_test.go b/cmd/harbor/root/jobservice/schedules/list_test.go new file mode 100644 index 000000000..92fd8fcaa --- /dev/null +++ b/cmd/harbor/root/jobservice/schedules/list_test.go @@ -0,0 +1,94 @@ +package schedules + +// 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. + +import ( + "errors" + "strings" + "testing" +) + +func TestSchedulesCommandIncludesExpectedSubcommands(t *testing.T) { + cmd := SchedulesCommand() + + got := make(map[string]struct{}, len(cmd.Commands())) + for _, subcommand := range cmd.Commands() { + got[subcommand.Name()] = struct{}{} + } + + for _, want := range []string{"list", "status", "pause-all", "resume-all"} { + if _, ok := got[want]; !ok { + t.Fatalf("expected subcommand %q to be registered", want) + } + } +} + +func TestListCommandDefaults(t *testing.T) { + cmd := ListCommand() + + if got := cmd.Flags().Lookup("page").DefValue; got != "1" { + t.Fatalf("expected default page to be 1, got %s", got) + } + + if got := cmd.Flags().Lookup("page-size").DefValue; got != "20" { + t.Fatalf("expected default page-size to be 20, got %s", got) + } +} + +func TestFormatScheduleError(t *testing.T) { + tests := []struct { + name string + err error + permission string + wantSubstr string + }{ + { + name: "bad request", + err: errors.New("[GET /schedule][400] (status 400)"), + permission: "ActionStop", + wantSubstr: "invalid request", + }, + { + name: "authentication required", + err: errors.New("[GET /schedule][401] (status 401)"), + permission: "ActionStop", + wantSubstr: "authentication required", + }, + { + name: "authenticated but forbidden", + err: errors.New("[GET /schedule][403] (status 403)"), + permission: "authenticated", + wantSubstr: "authenticated but lacks access", + }, + { + name: "fallback message", + err: errors.New("unexpected failure"), + permission: "ActionStop", + wantSubstr: "unexpected failure", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := formatScheduleError("failed to test", tt.err, tt.permission) + if got == nil { + t.Fatal("expected an error") + } + if !strings.Contains(got.Error(), tt.wantSubstr) { + t.Fatalf("expected %q to contain %q", got.Error(), tt.wantSubstr) + } + }) + } +} diff --git a/doc/cli-docs/harbor-jobservice-schedules-list.md b/doc/cli-docs/harbor-jobservice-schedules-list.md new file mode 100644 index 000000000..e96b560fe --- /dev/null +++ b/doc/cli-docs/harbor-jobservice-schedules-list.md @@ -0,0 +1,44 @@ +--- +title: harbor jobservice schedules list +weight: 10 +--- +## harbor jobservice schedules list + +### Description + +##### List schedules (supports --page and --page-size) + +### Synopsis + +Display all job schedules with pagination support. + +```sh +harbor jobservice schedules list [flags] +``` + +### Examples + +```sh +harbor jobservice schedules list --page 1 --page-size 20 +``` + +### Options + +```sh + -h, --help help for list + --page int Page number (default 1) + --page-size int Number of items per page (default 20) +``` + +### 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 + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor jobservice schedules](harbor-jobservice-schedules.md) - Manage schedules (list/status/pause-all/resume-all) + diff --git a/doc/cli-docs/harbor-jobservice-schedules-pause-all.md b/doc/cli-docs/harbor-jobservice-schedules-pause-all.md new file mode 100644 index 000000000..d95aade7c --- /dev/null +++ b/doc/cli-docs/harbor-jobservice-schedules-pause-all.md @@ -0,0 +1,42 @@ +--- +title: harbor jobservice schedules pause all +weight: 95 +--- +## harbor jobservice schedules pause-all + +### Description + +##### Pause all schedules + +### Synopsis + +Pause the global scheduler and all schedules. + +```sh +harbor jobservice schedules pause-all [flags] +``` + +### Examples + +```sh +harbor jobservice schedules pause-all +``` + +### Options + +```sh + -h, --help help for pause-all +``` + +### 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 + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor jobservice schedules](harbor-jobservice-schedules.md) - Manage schedules (list/status/pause-all/resume-all) + diff --git a/doc/cli-docs/harbor-jobservice-schedules-resume-all.md b/doc/cli-docs/harbor-jobservice-schedules-resume-all.md new file mode 100644 index 000000000..d6b3d6dae --- /dev/null +++ b/doc/cli-docs/harbor-jobservice-schedules-resume-all.md @@ -0,0 +1,42 @@ +--- +title: harbor jobservice schedules resume all +weight: 90 +--- +## harbor jobservice schedules resume-all + +### Description + +##### Resume all schedules + +### Synopsis + +Resume the global scheduler and all schedules. + +```sh +harbor jobservice schedules resume-all [flags] +``` + +### Examples + +```sh +harbor jobservice schedules resume-all +``` + +### Options + +```sh + -h, --help help for resume-all +``` + +### 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 + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor jobservice schedules](harbor-jobservice-schedules.md) - Manage schedules (list/status/pause-all/resume-all) + diff --git a/doc/cli-docs/harbor-jobservice-schedules-status.md b/doc/cli-docs/harbor-jobservice-schedules-status.md new file mode 100644 index 000000000..94e1db5a5 --- /dev/null +++ b/doc/cli-docs/harbor-jobservice-schedules-status.md @@ -0,0 +1,42 @@ +--- +title: harbor jobservice schedules status +weight: 70 +--- +## harbor jobservice schedules status + +### Description + +##### Show scheduler status + +### Synopsis + +Display whether the global scheduler is paused or running. + +```sh +harbor jobservice schedules status [flags] +``` + +### Examples + +```sh +harbor jobservice schedules status +``` + +### Options + +```sh + -h, --help help for status +``` + +### 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 + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor jobservice schedules](harbor-jobservice-schedules.md) - Manage schedules (list/status/pause-all/resume-all) + diff --git a/doc/cli-docs/harbor-jobservice-schedules.md b/doc/cli-docs/harbor-jobservice-schedules.md new file mode 100644 index 000000000..536fc0c0f --- /dev/null +++ b/doc/cli-docs/harbor-jobservice-schedules.md @@ -0,0 +1,36 @@ +--- +title: harbor jobservice schedules +weight: 5 +--- +## harbor jobservice schedules + +### Description + +##### Manage schedules (list/status/pause-all/resume-all) + +### Synopsis + +List schedules and manage global scheduler status. + +### Options + +```sh + -h, --help help for schedules +``` + +### 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 + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor jobservice](harbor-jobservice.md) - Manage Harbor job service (admin only) +* [harbor jobservice schedules list](harbor-jobservice-schedules-list.md) - List schedules (supports --page and --page-size) +* [harbor jobservice schedules pause-all](harbor-jobservice-schedules-pause-all.md) - Pause all schedules +* [harbor jobservice schedules resume-all](harbor-jobservice-schedules-resume-all.md) - Resume all schedules +* [harbor jobservice schedules status](harbor-jobservice-schedules-status.md) - Show scheduler status + diff --git a/doc/cli-docs/harbor-jobservice.md b/doc/cli-docs/harbor-jobservice.md new file mode 100644 index 000000000..ac9d7be3e --- /dev/null +++ b/doc/cli-docs/harbor-jobservice.md @@ -0,0 +1,36 @@ +--- +title: harbor jobservice +weight: 15 +--- +## harbor jobservice + +### Description + +##### Manage Harbor job service (admin only) + +### Synopsis + +Manage Harbor job service components including worker pools, job queues, schedules, and job logs. +This requires system admin privileges. + +Use "harbor jobservice [command] --help" for detailed examples and flags per subcommand. + +### Options + +```sh + -h, --help help for jobservice +``` + +### 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 + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor](harbor.md) - Official Harbor CLI +* [harbor jobservice schedules](harbor-jobservice-schedules.md) - Manage schedules (list/status/pause-all/resume-all) + diff --git a/doc/cli-docs/harbor.md b/doc/cli-docs/harbor.md index fe1916f83..13b4f5420 100644 --- a/doc/cli-docs/harbor.md +++ b/doc/cli-docs/harbor.md @@ -42,6 +42,7 @@ harbor help * [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 +* [harbor jobservice](harbor-jobservice.md) - Manage Harbor job service (admin only) * [harbor label](harbor-label.md) - Manage labels in Harbor * [harbor ldap](harbor-ldap.md) - Manage ldap users and groups * [harbor login](harbor-login.md) - Log in to Harbor registry diff --git a/doc/man-docs/man1/harbor-jobservice-schedules-list.1 b/doc/man-docs/man1/harbor-jobservice-schedules-list.1 new file mode 100644 index 000000000..6cdf50b8d --- /dev/null +++ b/doc/man-docs/man1/harbor-jobservice-schedules-list.1 @@ -0,0 +1,49 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-jobservice-schedules-list - List schedules (supports --page and --page-size) + + +.SH SYNOPSIS +\fBharbor jobservice schedules list [flags]\fP + + +.SH DESCRIPTION +Display all job schedules with pagination support. + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for list + +.PP +\fB--page\fP=1 + Page number + +.PP +\fB--page-size\fP=20 + Number of items per page + + +.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 + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX +harbor jobservice schedules list --page 1 --page-size 20 +.EE + + +.SH SEE ALSO +\fBharbor-jobservice-schedules(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-jobservice-schedules-pause-all.1 b/doc/man-docs/man1/harbor-jobservice-schedules-pause-all.1 new file mode 100644 index 000000000..8715a2c0d --- /dev/null +++ b/doc/man-docs/man1/harbor-jobservice-schedules-pause-all.1 @@ -0,0 +1,41 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-jobservice-schedules-pause-all - Pause all schedules + + +.SH SYNOPSIS +\fBharbor jobservice schedules pause-all [flags]\fP + + +.SH DESCRIPTION +Pause the global scheduler and all schedules. + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for pause-all + + +.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 + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX +harbor jobservice schedules pause-all +.EE + + +.SH SEE ALSO +\fBharbor-jobservice-schedules(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-jobservice-schedules-resume-all.1 b/doc/man-docs/man1/harbor-jobservice-schedules-resume-all.1 new file mode 100644 index 000000000..647494f79 --- /dev/null +++ b/doc/man-docs/man1/harbor-jobservice-schedules-resume-all.1 @@ -0,0 +1,41 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-jobservice-schedules-resume-all - Resume all schedules + + +.SH SYNOPSIS +\fBharbor jobservice schedules resume-all [flags]\fP + + +.SH DESCRIPTION +Resume the global scheduler and all schedules. + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for resume-all + + +.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 + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX +harbor jobservice schedules resume-all +.EE + + +.SH SEE ALSO +\fBharbor-jobservice-schedules(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-jobservice-schedules-status.1 b/doc/man-docs/man1/harbor-jobservice-schedules-status.1 new file mode 100644 index 000000000..eb1dfe9db --- /dev/null +++ b/doc/man-docs/man1/harbor-jobservice-schedules-status.1 @@ -0,0 +1,41 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-jobservice-schedules-status - Show scheduler status + + +.SH SYNOPSIS +\fBharbor jobservice schedules status [flags]\fP + + +.SH DESCRIPTION +Display whether the global scheduler is paused or running. + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for status + + +.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 + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH EXAMPLE +.EX +harbor jobservice schedules status +.EE + + +.SH SEE ALSO +\fBharbor-jobservice-schedules(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-jobservice-schedules.1 b/doc/man-docs/man1/harbor-jobservice-schedules.1 new file mode 100644 index 000000000..ae580e717 --- /dev/null +++ b/doc/man-docs/man1/harbor-jobservice-schedules.1 @@ -0,0 +1,35 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-jobservice-schedules - Manage schedules (list/status/pause-all/resume-all) + + +.SH SYNOPSIS +\fBharbor jobservice schedules [flags]\fP + + +.SH DESCRIPTION +List schedules and manage global scheduler status. + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for schedules + + +.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 + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH SEE ALSO +\fBharbor-jobservice(1)\fP, \fBharbor-jobservice-schedules-list(1)\fP, \fBharbor-jobservice-schedules-pause-all(1)\fP, \fBharbor-jobservice-schedules-resume-all(1)\fP, \fBharbor-jobservice-schedules-status(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-jobservice.1 b/doc/man-docs/man1/harbor-jobservice.1 new file mode 100644 index 000000000..d220f2cb1 --- /dev/null +++ b/doc/man-docs/man1/harbor-jobservice.1 @@ -0,0 +1,39 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-jobservice - Manage Harbor job service (admin only) + + +.SH SYNOPSIS +\fBharbor jobservice [flags]\fP + + +.SH DESCRIPTION +Manage Harbor job service components including worker pools, job queues, schedules, and job logs. +This requires system admin privileges. + +.PP +Use "harbor jobservice [command] --help" for detailed examples and flags per subcommand. + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for jobservice + + +.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 + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + verbose output + + +.SH SEE ALSO +\fBharbor(1)\fP, \fBharbor-jobservice-schedules(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 cd0884420..f39b7dfbe 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-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-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/jobservice_handler.go b/pkg/api/jobservice_handler.go new file mode 100644 index 000000000..5a1e7d966 --- /dev/null +++ b/pkg/api/jobservice_handler.go @@ -0,0 +1,73 @@ +// 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/jobservice" + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/schedule" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" +) + +// ActionJobQueue performs an action on a job queue (stop/pause/resume) +func ActionJobQueue(jobType, action string) error { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return err + } + + _, err = client.Jobservice.ActionPendingJobs(ctx, &jobservice.ActionPendingJobsParams{ + JobType: jobType, + ActionRequest: &models.ActionRequest{ + Action: action, + }, + }) + + return err +} + +// ListSchedules retrieves schedules with pagination +func ListSchedules(page, pageSize int64) (*schedule.ListSchedulesOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + + response, err := client.Schedule.ListSchedules(ctx, &schedule.ListSchedulesParams{ + Page: &page, + PageSize: &pageSize, + }) + if err != nil { + return nil, err + } + + return response, nil +} + +// GetSchedulePaused retrieves the global scheduler paused status +func GetSchedulePaused() (*schedule.GetSchedulePausedOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + + response, err := client.Schedule.GetSchedulePaused(ctx, &schedule.GetSchedulePausedParams{ + JobType: "all", + }) + if err != nil { + return nil, err + } + + return response, nil +} diff --git a/pkg/views/jobservice/schedules/view.go b/pkg/views/jobservice/schedules/view.go new file mode 100644 index 000000000..6ccee13e4 --- /dev/null +++ b/pkg/views/jobservice/schedules/view.go @@ -0,0 +1,55 @@ +// 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 schedules + +import ( + "fmt" + + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" +) + +// ListSchedules displays schedule tasks with pagination metadata. +func ListSchedules(items []*models.ScheduleTask, page, pageSize, totalCount int64) { + if len(items) == 0 { + fmt.Println("No schedules found.") + return + } + + fmt.Printf("%-8s %-18s %-22s %-30s %-20s\n", "ID", "VENDOR_TYPE", "VENDOR_ID", "CRON", "UPDATE_TIME") + fmt.Printf("%-8s %-18s %-22s %-30s %-20s\n", "--", "-----------", "---------", "----", "-----------") + + for _, task := range items { + if task == nil { + continue + } + fmt.Printf("%-8d %-18s %-22d %-30s %-20s\n", task.ID, task.VendorType, task.VendorID, task.Cron, task.UpdateTime.String()) + } + + fmt.Printf("\nPage: %d Page Size: %d Returned: %d Total: %d\n", page, pageSize, len(items), totalCount) +} + +// PrintScheduleStatus displays the scheduler paused/running state. +func PrintScheduleStatus(status *models.SchedulerStatus) { + if status == nil { + fmt.Println("Scheduler status: unknown") + return + } + + if status.Paused { + fmt.Println("Scheduler status: paused") + return + } + + fmt.Println("Scheduler status: running") +} diff --git a/pkg/views/jobservice/schedules/view_test.go b/pkg/views/jobservice/schedules/view_test.go new file mode 100644 index 000000000..305c5827d --- /dev/null +++ b/pkg/views/jobservice/schedules/view_test.go @@ -0,0 +1,117 @@ +package schedules + +// 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. + +import ( + "bytes" + "io" + "os" + "strings" + "testing" + + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" +) + +func captureOutput(t *testing.T, fn func()) string { + t.Helper() + + originalStdout := os.Stdout + reader, writer, err := os.Pipe() + if err != nil { + t.Fatalf("failed to create pipe: %v", err) + } + + os.Stdout = writer + defer func() { + os.Stdout = originalStdout + }() + + outputCh := make(chan string, 1) + go func() { + var buffer bytes.Buffer + _, _ = io.Copy(&buffer, reader) + outputCh <- buffer.String() + }() + + fn() + + _ = writer.Close() + return <-outputCh +} + +func TestListSchedules(t *testing.T) { + output := captureOutput(t, func() { + ListSchedules([]*models.ScheduleTask{ + { + ID: 42, + VendorType: "replication", + VendorID: 7, + Cron: "0 0 * * *", + }, + }, 2, 20, 1) + }) + + for _, want := range []string{"ID", "VENDOR_TYPE", "VENDOR_ID", "CRON", "UPDATE_TIME", "42", "replication", "7", "0 0 * * *", "Page: 2 Page Size: 20 Returned: 1 Total: 1"} { + if !strings.Contains(output, want) { + t.Fatalf("expected output to contain %q, got %q", want, output) + } + } +} + +func TestListSchedulesEmpty(t *testing.T) { + output := captureOutput(t, func() { + ListSchedules(nil, 1, 20, 0) + }) + + if !strings.Contains(output, "No schedules found.") { + t.Fatalf("expected empty-state message, got %q", output) + } +} + +func TestPrintScheduleStatus(t *testing.T) { + tests := []struct { + name string + in *models.SchedulerStatus + want string + }{ + { + name: "nil", + in: nil, + want: "Scheduler status: unknown", + }, + { + name: "paused", + in: &models.SchedulerStatus{Paused: true}, + want: "Scheduler status: paused", + }, + { + name: "running", + in: &models.SchedulerStatus{Paused: false}, + want: "Scheduler status: running", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output := captureOutput(t, func() { + PrintScheduleStatus(tt.in) + }) + + if !strings.Contains(output, tt.want) { + t.Fatalf("expected output to contain %q, got %q", tt.want, output) + } + }) + } +} From cb8829c301d80164aff121c7ee1c8e6fdd661306 Mon Sep 17 00:00:00 2001 From: Nishchay Rajput Date: Sat, 4 Apr 2026 14:24:28 +0530 Subject: [PATCH 2/3] fix: moved subcommands to seperate files Signed-off-by: Nishchay Rajput --- cmd/harbor/root/jobservice/schedules/cmd.go | 34 +++++ cmd/harbor/root/jobservice/schedules/list.go | 119 ------------------ .../root/jobservice/schedules/list_test.go | 94 -------------- .../root/jobservice/schedules/pause_all.go | 42 +++++++ .../root/jobservice/schedules/resume_all.go | 42 +++++++ .../root/jobservice/schedules/status.go | 55 ++++++++ cmd/harbor/root/jobservice/schedules/utils.go | 48 +++++++ 7 files changed, 221 insertions(+), 213 deletions(-) create mode 100644 cmd/harbor/root/jobservice/schedules/cmd.go delete mode 100644 cmd/harbor/root/jobservice/schedules/list_test.go create mode 100644 cmd/harbor/root/jobservice/schedules/pause_all.go create mode 100644 cmd/harbor/root/jobservice/schedules/resume_all.go create mode 100644 cmd/harbor/root/jobservice/schedules/status.go create mode 100644 cmd/harbor/root/jobservice/schedules/utils.go diff --git a/cmd/harbor/root/jobservice/schedules/cmd.go b/cmd/harbor/root/jobservice/schedules/cmd.go new file mode 100644 index 000000000..8b5055b47 --- /dev/null +++ b/cmd/harbor/root/jobservice/schedules/cmd.go @@ -0,0 +1,34 @@ +// 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 schedules + +import "github.com/spf13/cobra" + +// SchedulesCommand creates the schedules subcommand +func SchedulesCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "schedules", + Short: "Manage schedules (list/status/pause-all/resume-all)", + Long: "List schedules and manage global scheduler status.", + } + + cmd.AddCommand( + ListCommand(), + StatusCommand(), + PauseAllCommand(), + ResumeAllCommand(), + ) + + return cmd +} diff --git a/cmd/harbor/root/jobservice/schedules/list.go b/cmd/harbor/root/jobservice/schedules/list.go index 88b4f931d..53bdac4d3 100644 --- a/cmd/harbor/root/jobservice/schedules/list.go +++ b/cmd/harbor/root/jobservice/schedules/list.go @@ -23,24 +23,6 @@ import ( "github.com/spf13/viper" ) -// SchedulesCommand creates the schedules subcommand -func SchedulesCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "schedules", - Short: "Manage schedules (list/status/pause-all/resume-all)", - Long: "List schedules and manage global scheduler status.", - } - - cmd.AddCommand( - ListCommand(), - StatusCommand(), - PauseAllCommand(), - ResumeAllCommand(), - ) - - return cmd -} - // ListCommand lists all schedules func ListCommand() *cobra.Command { var page int64 = 1 @@ -86,104 +68,3 @@ func ListCommand() *cobra.Command { return cmd } - -// StatusCommand shows the global scheduler status -func StatusCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "status", - Short: "Show scheduler status", - Long: "Display whether the global scheduler is paused or running.", - Example: "harbor jobservice schedules status", - RunE: func(cmd *cobra.Command, args []string) error { - response, err := api.GetSchedulePaused() - if err != nil { - return formatScheduleError("failed to retrieve scheduler status", err, "authenticated") - } - - if response == nil || response.Payload == nil { - fmt.Println("Unable to determine scheduler status.") - return nil - } - - formatFlag := viper.GetString("output-format") - if formatFlag != "" { - return utils.PrintFormat(response.Payload, formatFlag) - } - - schedules.PrintScheduleStatus(response.Payload) - return nil - }, - } - - return cmd -} - -// PauseAllCommand pauses all schedules -func PauseAllCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "pause-all", - Short: "Pause all schedules", - Long: "Pause the global scheduler and all schedules.", - Example: "harbor jobservice schedules pause-all", - RunE: func(cmd *cobra.Command, args []string) error { - fmt.Println("Pausing all schedules...") - err := api.ActionJobQueue("SCHEDULER", "pause") - if err != nil { - return formatScheduleError("failed to pause all schedules", err, "ActionStop") - } - fmt.Println("✓ All schedules paused successfully.") - return nil - }, - } - - return cmd -} - -// ResumeAllCommand resumes all schedules -func ResumeAllCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "resume-all", - Short: "Resume all schedules", - Long: "Resume the global scheduler and all schedules.", - Example: "harbor jobservice schedules resume-all", - RunE: func(cmd *cobra.Command, args []string) error { - fmt.Println("Resuming all schedules...") - err := api.ActionJobQueue("SCHEDULER", "resume") - if err != nil { - return formatScheduleError("failed to resume all schedules", err, "ActionStop") - } - fmt.Println("✓ All schedules resumed successfully.") - return nil - }, - } - - return cmd -} - -func formatScheduleError(operation string, err error, requiredPermission string) error { - errorCode := utils.ParseHarborErrorCode(err) - - switch errorCode { - case "400": - return fmt.Errorf("%s: invalid request. For schedule status use job_type=all; for queue action use stop|pause|resume", operation) - case "401": - return fmt.Errorf("%s: authentication required. Please run 'harbor login' and try again", operation) - case "403": - if requiredPermission == "authenticated" { - return fmt.Errorf("%s: permission denied. Your account is authenticated but lacks access", operation) - } - return fmt.Errorf("%s: permission denied. This operation requires %s on jobservice-monitor", operation, requiredPermission) - case "404": - return fmt.Errorf("%s: resource not found or not accessible in current context", operation) - case "422": - return fmt.Errorf("%s: request validation failed. Please check request body and action values", operation) - case "500": - return fmt.Errorf("%s: Harbor internal error. Retry and check Harbor server logs", operation) - default: - msg := utils.ParseHarborErrorMsg(err) - if msg == "" { - msg = err.Error() - } - return fmt.Errorf("%s: %s", operation, msg) - } -} diff --git a/cmd/harbor/root/jobservice/schedules/list_test.go b/cmd/harbor/root/jobservice/schedules/list_test.go deleted file mode 100644 index 92fd8fcaa..000000000 --- a/cmd/harbor/root/jobservice/schedules/list_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package schedules - -// 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. - -import ( - "errors" - "strings" - "testing" -) - -func TestSchedulesCommandIncludesExpectedSubcommands(t *testing.T) { - cmd := SchedulesCommand() - - got := make(map[string]struct{}, len(cmd.Commands())) - for _, subcommand := range cmd.Commands() { - got[subcommand.Name()] = struct{}{} - } - - for _, want := range []string{"list", "status", "pause-all", "resume-all"} { - if _, ok := got[want]; !ok { - t.Fatalf("expected subcommand %q to be registered", want) - } - } -} - -func TestListCommandDefaults(t *testing.T) { - cmd := ListCommand() - - if got := cmd.Flags().Lookup("page").DefValue; got != "1" { - t.Fatalf("expected default page to be 1, got %s", got) - } - - if got := cmd.Flags().Lookup("page-size").DefValue; got != "20" { - t.Fatalf("expected default page-size to be 20, got %s", got) - } -} - -func TestFormatScheduleError(t *testing.T) { - tests := []struct { - name string - err error - permission string - wantSubstr string - }{ - { - name: "bad request", - err: errors.New("[GET /schedule][400] (status 400)"), - permission: "ActionStop", - wantSubstr: "invalid request", - }, - { - name: "authentication required", - err: errors.New("[GET /schedule][401] (status 401)"), - permission: "ActionStop", - wantSubstr: "authentication required", - }, - { - name: "authenticated but forbidden", - err: errors.New("[GET /schedule][403] (status 403)"), - permission: "authenticated", - wantSubstr: "authenticated but lacks access", - }, - { - name: "fallback message", - err: errors.New("unexpected failure"), - permission: "ActionStop", - wantSubstr: "unexpected failure", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := formatScheduleError("failed to test", tt.err, tt.permission) - if got == nil { - t.Fatal("expected an error") - } - if !strings.Contains(got.Error(), tt.wantSubstr) { - t.Fatalf("expected %q to contain %q", got.Error(), tt.wantSubstr) - } - }) - } -} diff --git a/cmd/harbor/root/jobservice/schedules/pause_all.go b/cmd/harbor/root/jobservice/schedules/pause_all.go new file mode 100644 index 000000000..167c4ebf7 --- /dev/null +++ b/cmd/harbor/root/jobservice/schedules/pause_all.go @@ -0,0 +1,42 @@ +// 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 schedules + +import ( + "fmt" + + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/spf13/cobra" +) + +// PauseAllCommand pauses all schedules +func PauseAllCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "pause-all", + Short: "Pause all schedules", + Long: "Pause the global scheduler and all schedules.", + Example: "harbor jobservice schedules pause-all", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("Pausing all schedules...") + err := api.ActionJobQueue("SCHEDULER", "pause") + if err != nil { + return formatScheduleError("failed to pause all schedules", err, "ActionStop") + } + fmt.Println("✓ All schedules paused successfully.") + return nil + }, + } + + return cmd +} diff --git a/cmd/harbor/root/jobservice/schedules/resume_all.go b/cmd/harbor/root/jobservice/schedules/resume_all.go new file mode 100644 index 000000000..1b475db7f --- /dev/null +++ b/cmd/harbor/root/jobservice/schedules/resume_all.go @@ -0,0 +1,42 @@ +// 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 schedules + +import ( + "fmt" + + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/spf13/cobra" +) + +// ResumeAllCommand resumes all schedules +func ResumeAllCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "resume-all", + Short: "Resume all schedules", + Long: "Resume the global scheduler and all schedules.", + Example: "harbor jobservice schedules resume-all", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("Resuming all schedules...") + err := api.ActionJobQueue("SCHEDULER", "resume") + if err != nil { + return formatScheduleError("failed to resume all schedules", err, "ActionStop") + } + fmt.Println("✓ All schedules resumed successfully.") + return nil + }, + } + + return cmd +} diff --git a/cmd/harbor/root/jobservice/schedules/status.go b/cmd/harbor/root/jobservice/schedules/status.go new file mode 100644 index 000000000..acfa6ce3c --- /dev/null +++ b/cmd/harbor/root/jobservice/schedules/status.go @@ -0,0 +1,55 @@ +// 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 schedules + +import ( + "fmt" + + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/jobservice/schedules" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// StatusCommand shows the global scheduler status +func StatusCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "status", + Short: "Show scheduler status", + Long: "Display whether the global scheduler is paused or running.", + Example: "harbor jobservice schedules status", + RunE: func(cmd *cobra.Command, args []string) error { + response, err := api.GetSchedulePaused() + if err != nil { + return formatScheduleError("failed to retrieve scheduler status", err, "authenticated") + } + + if response == nil || response.Payload == nil { + fmt.Println("Unable to determine scheduler status.") + return nil + } + + formatFlag := viper.GetString("output-format") + if formatFlag != "" { + return utils.PrintFormat(response.Payload, formatFlag) + } + + schedules.PrintScheduleStatus(response.Payload) + return nil + }, + } + + return cmd +} diff --git a/cmd/harbor/root/jobservice/schedules/utils.go b/cmd/harbor/root/jobservice/schedules/utils.go new file mode 100644 index 000000000..68d9deadb --- /dev/null +++ b/cmd/harbor/root/jobservice/schedules/utils.go @@ -0,0 +1,48 @@ +// 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 schedules + +import ( + "fmt" + + "github.com/goharbor/harbor-cli/pkg/utils" +) + +func formatScheduleError(operation string, err error, requiredPermission string) error { + errorCode := utils.ParseHarborErrorCode(err) + + switch errorCode { + case "400": + return fmt.Errorf("%s: invalid request. For schedule status use job_type=all; for queue action use stop|pause|resume", operation) + case "401": + return fmt.Errorf("%s: authentication required. Please run 'harbor login' and try again", operation) + case "403": + if requiredPermission == "authenticated" { + return fmt.Errorf("%s: permission denied. Your account is authenticated but lacks access", operation) + } + return fmt.Errorf("%s: permission denied. This operation requires %s on jobservice-monitor", operation, requiredPermission) + case "404": + return fmt.Errorf("%s: resource not found or not accessible in current context", operation) + case "422": + return fmt.Errorf("%s: request validation failed. Please check request body and action values", operation) + case "500": + return fmt.Errorf("%s: Harbor internal error. Retry and check Harbor server logs", operation) + default: + msg := utils.ParseHarborErrorMsg(err) + if msg == "" { + msg = err.Error() + } + return fmt.Errorf("%s: %s", operation, msg) + } +} From 5fb39d8202413e08ba50e479af486c740b309f2f Mon Sep 17 00:00:00 2001 From: Nishchay Rajput Date: Tue, 7 Apr 2026 22:58:02 +0530 Subject: [PATCH 3/3] fix: lints and improve error message Signed-off-by: Nishchay Rajput --- cmd/harbor/root/jobservice/schedules/list.go | 3 ++- cmd/harbor/root/jobservice/schedules/pause_all.go | 3 ++- cmd/harbor/root/jobservice/schedules/resume_all.go | 3 ++- cmd/harbor/root/jobservice/schedules/status.go | 3 ++- .../schedules/utils.go => pkg/utils/jobservice/errors.go | 4 ++-- pkg/views/jobservice/schedules/view_test.go | 3 +-- 6 files changed, 11 insertions(+), 8 deletions(-) rename cmd/harbor/root/jobservice/schedules/utils.go => pkg/utils/jobservice/errors.go (95%) diff --git a/cmd/harbor/root/jobservice/schedules/list.go b/cmd/harbor/root/jobservice/schedules/list.go index 53bdac4d3..1c8c53b92 100644 --- a/cmd/harbor/root/jobservice/schedules/list.go +++ b/cmd/harbor/root/jobservice/schedules/list.go @@ -18,6 +18,7 @@ import ( "github.com/goharbor/harbor-cli/pkg/api" "github.com/goharbor/harbor-cli/pkg/utils" + jobserviceutils "github.com/goharbor/harbor-cli/pkg/utils/jobservice" "github.com/goharbor/harbor-cli/pkg/views/jobservice/schedules" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -43,7 +44,7 @@ func ListCommand() *cobra.Command { response, err := api.ListSchedules(page, pageSize) if err != nil { - return formatScheduleError("failed to retrieve schedules", err, "ActionList") + return jobserviceutils.FormatScheduleError("failed to retrieve schedules", err, "ActionList") } if response == nil || response.Payload == nil || len(response.Payload) == 0 { diff --git a/cmd/harbor/root/jobservice/schedules/pause_all.go b/cmd/harbor/root/jobservice/schedules/pause_all.go index 167c4ebf7..cb896d2bb 100644 --- a/cmd/harbor/root/jobservice/schedules/pause_all.go +++ b/cmd/harbor/root/jobservice/schedules/pause_all.go @@ -17,6 +17,7 @@ import ( "fmt" "github.com/goharbor/harbor-cli/pkg/api" + jobserviceutils "github.com/goharbor/harbor-cli/pkg/utils/jobservice" "github.com/spf13/cobra" ) @@ -31,7 +32,7 @@ func PauseAllCommand() *cobra.Command { fmt.Println("Pausing all schedules...") err := api.ActionJobQueue("SCHEDULER", "pause") if err != nil { - return formatScheduleError("failed to pause all schedules", err, "ActionStop") + return jobserviceutils.FormatScheduleError("failed to pause all schedules", err, "ActionStop") } fmt.Println("✓ All schedules paused successfully.") return nil diff --git a/cmd/harbor/root/jobservice/schedules/resume_all.go b/cmd/harbor/root/jobservice/schedules/resume_all.go index 1b475db7f..b043f5097 100644 --- a/cmd/harbor/root/jobservice/schedules/resume_all.go +++ b/cmd/harbor/root/jobservice/schedules/resume_all.go @@ -17,6 +17,7 @@ import ( "fmt" "github.com/goharbor/harbor-cli/pkg/api" + jobserviceutils "github.com/goharbor/harbor-cli/pkg/utils/jobservice" "github.com/spf13/cobra" ) @@ -31,7 +32,7 @@ func ResumeAllCommand() *cobra.Command { fmt.Println("Resuming all schedules...") err := api.ActionJobQueue("SCHEDULER", "resume") if err != nil { - return formatScheduleError("failed to resume all schedules", err, "ActionStop") + return jobserviceutils.FormatScheduleError("failed to resume all schedules", err, "ActionStop") } fmt.Println("✓ All schedules resumed successfully.") return nil diff --git a/cmd/harbor/root/jobservice/schedules/status.go b/cmd/harbor/root/jobservice/schedules/status.go index acfa6ce3c..f8448af54 100644 --- a/cmd/harbor/root/jobservice/schedules/status.go +++ b/cmd/harbor/root/jobservice/schedules/status.go @@ -18,6 +18,7 @@ import ( "github.com/goharbor/harbor-cli/pkg/api" "github.com/goharbor/harbor-cli/pkg/utils" + jobserviceutils "github.com/goharbor/harbor-cli/pkg/utils/jobservice" "github.com/goharbor/harbor-cli/pkg/views/jobservice/schedules" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -33,7 +34,7 @@ func StatusCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { response, err := api.GetSchedulePaused() if err != nil { - return formatScheduleError("failed to retrieve scheduler status", err, "authenticated") + return jobserviceutils.FormatScheduleError("failed to retrieve scheduler status", err, "authenticated") } if response == nil || response.Payload == nil { diff --git a/cmd/harbor/root/jobservice/schedules/utils.go b/pkg/utils/jobservice/errors.go similarity index 95% rename from cmd/harbor/root/jobservice/schedules/utils.go rename to pkg/utils/jobservice/errors.go index 68d9deadb..a7c222ee0 100644 --- a/cmd/harbor/root/jobservice/schedules/utils.go +++ b/pkg/utils/jobservice/errors.go @@ -11,7 +11,7 @@ // 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 schedules +package jobservice import ( "fmt" @@ -19,7 +19,7 @@ import ( "github.com/goharbor/harbor-cli/pkg/utils" ) -func formatScheduleError(operation string, err error, requiredPermission string) error { +func FormatScheduleError(operation string, err error, requiredPermission string) error { errorCode := utils.ParseHarborErrorCode(err) switch errorCode { diff --git a/pkg/views/jobservice/schedules/view_test.go b/pkg/views/jobservice/schedules/view_test.go index 305c5827d..ed19d9308 100644 --- a/pkg/views/jobservice/schedules/view_test.go +++ b/pkg/views/jobservice/schedules/view_test.go @@ -1,5 +1,3 @@ -package schedules - // Copyright Project Harbor Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,6 +11,7 @@ package schedules // 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 schedules import ( "bytes"