Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/harbor/root/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
41 changes: 41 additions & 0 deletions cmd/harbor/root/jobservice/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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/jobs"
"github.com/goharbor/harbor-cli/cmd/harbor/root/jobservice/pools"
"github.com/goharbor/harbor-cli/cmd/harbor/root/jobservice/workers"
"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(
pools.PoolsCommand(),
workers.WorkersCommand(),
jobs.JobsCommand(),
)

return cmd
}
74 changes: 74 additions & 0 deletions cmd/harbor/root/jobservice/jobs/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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 jobs

import (
"fmt"

"github.com/goharbor/harbor-cli/pkg/api"
"github.com/spf13/cobra"
)

// JobsCommand creates the jobs subcommand
func JobsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "jobs",
Short: "Manage job logs (view by job ID)",
Long: "View logs for specific jobs.",
}

cmd.AddCommand(LogCommand())

return cmd
}

// LogCommand retrieves and displays job logs
func LogCommand() *cobra.Command {
var jobID string

cmd := &cobra.Command{
Use: "log",
Short: "View a job log (--job-id required)",
Long: "Display the log for a specific job by job ID.",
Example: "harbor jobservice jobs log --job-id abc123def456",
RunE: func(cmd *cobra.Command, args []string) error {
if jobID == "" {
return fmt.Errorf("--job-id must be specified")
}

fmt.Printf("Retrieving log for job %s...\n\n", jobID)

log, err := api.GetJobLog(jobID)
if err != nil {
return fmt.Errorf("failed to retrieve job log: %w", err)
}

if log == "" {
fmt.Println("No log content available for this job.")
return nil
}

fmt.Println("=== Job Log ===")
fmt.Println(log)
fmt.Println("=== End of Log ===")
return nil
},
}

flags := cmd.Flags()
flags.StringVar(&jobID, "job-id", "", "Job ID to fetch log for (required)")
cmd.MarkFlagRequired("job-id")

return cmd
}
41 changes: 41 additions & 0 deletions cmd/harbor/root/jobservice/jobs/log_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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 jobs

import "testing"

func TestJobsCommandIncludesLogSubcommand(t *testing.T) {
cmd := JobsCommand()

if cmd.Use != "jobs" {
t.Fatalf("expected command use to be jobs, got %s", cmd.Use)
}

if len(cmd.Commands()) != 1 {
t.Fatalf("expected one subcommand, got %d", len(cmd.Commands()))
}

if got := cmd.Commands()[0].Name(); got != "log" {
t.Fatalf("expected log subcommand, got %s", got)
}
}

func TestLogCommandRequiresJobID(t *testing.T) {
cmd := LogCommand()
cmd.SetArgs([]string{})
if err := cmd.Execute(); err == nil || err.Error() != "--job-id must be specified" {
t.Fatalf("expected job-id validation error, got %v", err)
}
}
74 changes: 74 additions & 0 deletions cmd/harbor/root/jobservice/pools/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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 pools

import (
"fmt"

"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
poolviews "github.com/goharbor/harbor-cli/pkg/views/jobservice/pools"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

// PoolsCommand creates the pools subcommand.
func PoolsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "pools",
Aliases: []string{"pool"},
Short: "Manage worker pools (list available pools)",
Long: `List and manage worker pools for the Harbor job service.

Use 'list' to view all worker pools.

Examples:
harbor jobservice pools list
harbor jobservice pool list`,
}

cmd.AddCommand(ListCommand())
return cmd
}

// ListCommand lists all worker pools.
func ListCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List all worker pools",
Long: "Display all worker pools with their details.",
Example: "harbor jobservice pools list\nharbor jobservice pool list",
RunE: func(cmd *cobra.Command, args []string) error {
response, err := api.GetWorkerPools()
if err != nil {
return fmt.Errorf("failed to retrieve worker pools: %w", err)
}

if response == nil || response.Payload == nil || len(response.Payload) == 0 {
fmt.Println("No worker pools found.")
return nil
}

formatFlag := viper.GetString("output-format")
if formatFlag != "" {
return utils.PrintFormat(response.Payload, formatFlag)
}

poolviews.ListPools(response.Payload)
return nil
},
}

return cmd
}
45 changes: 45 additions & 0 deletions cmd/harbor/root/jobservice/pools/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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 pools

import "testing"

func TestPoolsCommandIncludesListSubcommand(t *testing.T) {
cmd := PoolsCommand()

if cmd.Use != "pools" {
t.Fatalf("expected command use to be pools, got %s", cmd.Use)
}

if len(cmd.Commands()) != 1 {
t.Fatalf("expected one subcommand, got %d", len(cmd.Commands()))
}

if got := cmd.Commands()[0].Name(); got != "list" {
t.Fatalf("expected list subcommand, got %s", got)
}
}

func TestListCommandName(t *testing.T) {
cmd := ListCommand()

if cmd.Name() != "list" {
t.Fatalf("expected list command, got %s", cmd.Name())
}

if cmd.Short == "" {
t.Fatal("expected short description to be populated")
}
}
94 changes: 94 additions & 0 deletions cmd/harbor/root/jobservice/workers/free.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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 workers

import (
"fmt"

"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/spf13/cobra"
)

// FreeCommand frees a worker by stopping the running job on it.
func FreeCommand() *cobra.Command {
var jobID string

cmd := &cobra.Command{
Use: "free",
Short: "Free one worker (--job-id required)",
Long: "Stop a running job by job ID to free its worker.",
Example: "harbor jobservice workers free --job-id abc123",
RunE: func(cmd *cobra.Command, args []string) error {
if jobID == "" {
return fmt.Errorf("--job-id is required")
}

err := api.StopRunningJob(jobID)
if err != nil {
return formatWorkerActionError("failed to free worker", err)
}

fmt.Printf("Worker job %q stopped successfully.\n", jobID)
return nil
},
}

cmd.Flags().StringVar(&jobID, "job-id", "", "Running job ID to stop")

return cmd
}

// FreeAllCommand frees all busy workers by stopping all running jobs.
func FreeAllCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "free-all",
Short: "Free all busy workers (job-id=all)",
Long: "Stop all running jobs to free all busy workers.",
Example: "harbor jobservice workers free-all",
RunE: func(cmd *cobra.Command, args []string) error {
err := api.StopRunningJob("all")
if err != nil {
return formatWorkerActionError("failed to free all workers", err)
}

fmt.Println("All busy workers were freed successfully.")
return nil
},
}

return cmd
}

func formatWorkerActionError(operation string, err error) error {
errorCode := utils.ParseHarborErrorCode(err)

switch errorCode {
case "401":
return fmt.Errorf("%s: authentication required. Please run 'harbor login' and try again", operation)
case "403":
return fmt.Errorf("%s: permission denied. This operation requires ActionStop on jobservice-monitor", operation)
case "404":
return fmt.Errorf("%s: job not found or already completed", 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)
}
}
Loading
Loading