Compare commits

...

15 Commits
v0.0.8 ... main

Author SHA1 Message Date
Vadim Bauer ff1485171c
chnage robot account to supported
Signed-off-by: Vadim Bauer <vb@container-registry.com>
2025-08-05 15:07:46 +02:00
RafsanNeloy e15bde4ba0
Typo solved (#522) 2025-08-01 18:18:25 +05:30
Patrick Eschenbach 3c8e6239a0
Add Support for System-Level Robot Accounts (#507) 2025-07-29 18:33:44 +05:30
dependabot[bot] 6caed4d4f4
build(deps): bump golang.org/x/text from 0.23.0 to 0.27.0 (#519)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.23.0 to 0.27.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.23.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-21 14:32:23 +05:30
dependabot[bot] 18cb2da313
build(deps): bump github.com/charmbracelet/bubbletea from 1.3.5 to 1.3.6 (#515) 2025-07-15 13:51:06 +00:00
dependabot[bot] 85618bdd6b
build(deps): bump golang.org/x/term from 0.32.0 to 0.33.0 (#514)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/term/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-15 15:46:31 +02:00
Patrick Eschenbach 0fbd8ecdea
Increased the logs follow refresh interval to 5s as a default (#516)
* Increased the logs follow refresh interval to 5s as a default

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Re-export docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-07-15 15:45:27 +02:00
Patrick Eschenbach c705221645
Replication Start/Stop Management (#501)
* First save

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication list command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Split replication policies to sub command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication policies view command; added get rep policies to prompt and handler

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Finished rpolicies view command; started create command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added create command; ToDo: finish all fields for creation

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* feat(replication): add commands to start and stop replication executions

- Implement start command to initiate policy execution
- Add stop command to halt running executions
- Create replication execution views
- Update API handler with execution management functions
- Extend prompt functionality for execution commands

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added long docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added changes for linter; removed unneeded funcs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-07-15 15:19:14 +02:00
Patrick Eschenbach cc2113c2dd
Replication Policy Management with Configuration File Support (#510)
* feat(replication): add complete replication policy management with config files

- YAML/JSON configuration file support
- Comprehensive filter validation (resource/name/tag/label)
- Enhanced policy viewing with separate filter tables
- Support for manual/scheduled/event-based triggers
- Example configs and documentation included

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Updated docs for linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-07-15 15:16:06 +02:00
Patrick Eschenbach 3304c2ea0f
feat(logs): add audit logs command with follow support (#511)
* add: audit-log command

Signed-off-by: bupd <bupdprasanth@gmail.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Update cmd/harbor/root/logs.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Vadim Bauer <Bauer.vadim@gmail.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* build(deps): bump github.com/goharbor/go-client from 0.210.0 to 0.213.1

Bumps [github.com/goharbor/go-client](https://github.com/goharbor/go-client) from 0.210.0 to 0.213.1.
- [Release notes](https://github.com/goharbor/go-client/releases)
- [Commits](https://github.com/goharbor/go-client/compare/v0.210.0...v0.213.1)

---
updated-dependencies:
- dependency-name: github.com/goharbor/go-client
  dependency-version: 0.213.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* fix(cli): adapt to go-client v0.213.1 changes

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added follow logs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Moved to AuditLogExt; follow does not yet work

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Logrus output is in verbose

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added trailing functionality to logs command; moved endpoint to ext audit log

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* fix: output formatting

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: bupd <bupdprasanth@gmail.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
Signed-off-by: Vadim Bauer <Bauer.vadim@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: bupd <bupdprasanth@gmail.com>
Co-authored-by: Vadim Bauer <Bauer.vadim@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-15 15:10:12 +02:00
dependabot[bot] fd38240593
build(deps): bump github.com/goharbor/go-client from 0.210.0 to 0.213.1 (#509)
* build(deps): bump github.com/goharbor/go-client from 0.210.0 to 0.213.1

Bumps [github.com/goharbor/go-client](https://github.com/goharbor/go-client) from 0.210.0 to 0.213.1.
- [Release notes](https://github.com/goharbor/go-client/releases)
- [Commits](https://github.com/goharbor/go-client/compare/v0.210.0...v0.213.1)

---
updated-dependencies:
- dependency-name: github.com/goharbor/go-client
  dependency-version: 0.213.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* fix(cli): adapt to go-client v0.213.1 changes

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-07-15 15:05:31 +02:00
Chethan 97fc4d6c64
Change the print statements in func preblock() (#513)
Signed-off-by: chethanm99 <chethanm1399@gmail.com>
2025-07-15 18:31:08 +05:30
Patrick Eschenbach 598b817a32
Replication Policy Management (#499)
* First save

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication list command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Split replication policies to sub command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication policies view command; added get rep policies to prompt and handler

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication policy delete command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Finished rpolicies view command; started create command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added create command; ToDo: finish all fields for creation

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication policies docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added replication policies update command

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Fix: left alignment in create and update view

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Fix: left alignment in create and update view

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Remove replication mode from update view

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added long docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added long docs

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* fix(replication): handle deprecated deletion field in policy updates

Set both ReplicateDeletion (new) and Deletion (deprecated) fields
to ensure compatibility with all Harbor API versions during policy
updates. The Deletion field will be removed in future Harbor versions.

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Added missing replication filters to creation and update view

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Export docs after rebase main

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Remove unnecessary stdout

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Put filters to their own table

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-07-02 17:00:19 +02:00
T3rm1n4t0r 95edbd6f8b
enhancing artifact view command (#485)
Co-authored-by: Prasanth Baskar <bupdprasanth@gmail.com>
2025-07-02 17:23:04 +05:30
Patrick Eschenbach af41e84a14
Enhanced Robot Account Configuration Format (#504)
* Added namespace and slighlty changed robot config spec

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Changed format of robot permission model to prepare for system robots

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Made changes to satisfy linter

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

* Changed kind to level

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>

---------

Signed-off-by: Patrick Eschenbach <patrickeschenbach96@gmail.com>
2025-07-02 08:49:30 +02:00
99 changed files with 6996 additions and 518 deletions

View File

@ -24,7 +24,7 @@ The project's first goal is to reach WebUI parity.
✅ tag Manage tags
✅ quota Manage quotas
✅ webhook Manage webhook policies
robot Robot Account
robot Robot Account
✅ login Log in to Harbor registry
✅ user Manage users
@ -62,7 +62,7 @@ docker run -ti --rm -v $HOME/.config/harbor-cli/config.yaml:/root/.config/harbor
```
Use the `HARBOR_ENCRYPTION_KEY` container environment variable as a base64-encoded 32-byte key for AES-256 encryption. This securely stores your harbor login password.
I you intend
If you intend
to run the CLI as a container,it is advised
to set the following environment variables and to create an alias
and append the alias to your .zshrc or .bashrc file

View File

@ -18,16 +18,17 @@ import (
"io"
"time"
"github.com/goharbor/harbor-cli/cmd/harbor/root/context"
"github.com/goharbor/harbor-cli/cmd/harbor/root/artifact"
"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/labels"
"github.com/goharbor/harbor-cli/cmd/harbor/root/project"
"github.com/goharbor/harbor-cli/cmd/harbor/root/quota"
"github.com/goharbor/harbor-cli/cmd/harbor/root/registry"
"github.com/goharbor/harbor-cli/cmd/harbor/root/replication"
"github.com/goharbor/harbor-cli/cmd/harbor/root/repository"
"github.com/goharbor/harbor-cli/cmd/harbor/root/robot"
"github.com/goharbor/harbor-cli/cmd/harbor/root/scan_all"
"github.com/goharbor/harbor-cli/cmd/harbor/root/scanner"
"github.com/goharbor/harbor-cli/cmd/harbor/root/schedule"
@ -137,6 +138,10 @@ harbor help
cmd.GroupID = "core"
root.AddCommand(cmd)
cmd = robot.Robot()
cmd.GroupID = "core"
root.AddCommand(cmd)
// Access
cmd = LoginCommand()
cmd.GroupID = "access"
@ -163,6 +168,10 @@ harbor help
cmd.GroupID = "system"
root.AddCommand(cmd)
cmd = replication.Replication()
cmd.GroupID = "system"
root.AddCommand(cmd)
cmd = scanner.Scanner()
cmd.GroupID = "system"
root.AddCommand(cmd)
@ -180,5 +189,9 @@ harbor help
cmd.GroupID = "utils"
root.AddCommand(cmd)
cmd = Logs()
cmd.GroupID = "utils"
root.AddCommand(cmd)
return root
}

217
cmd/harbor/root/logs.go Normal file
View File

@ -0,0 +1,217 @@
// 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 root
import (
"fmt"
"os"
"strings"
"time"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
list "github.com/goharbor/harbor-cli/pkg/views/logs"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var logsLogger = log.New()
func Logs() *cobra.Command {
var opts api.ListFlags
var follow bool
var refreshInterval string
cmd := &cobra.Command{
Use: "logs",
Short: "Get recent logs of the projects which the user is a member of",
Args: cobra.NoArgs,
Long: `Get recent logs of the projects which the user is a member of.
This command retrieves the audit logs for the projects the user is a member of. It supports pagination, sorting, and filtering through query parameters. The logs can be followed in real-time with the --follow flag, and the output can be formatted as JSON with the --output-format flag.
harbor-cli logs --page 1 --page-size 10 --query "operation=push" --sort "op_time:desc"
harbor-cli logs --follow --refresh-interval 2s
harbor-cli logs --output-format json`,
Run: func(cmd *cobra.Command, args []string) {
if refreshInterval != "" && !follow {
fmt.Println("The --refresh-interval flag is only applicable when using --follow. It will be ignored.")
}
if follow {
var interval time.Duration = 5 * time.Second
var err error
if refreshInterval != "" {
interval, err = time.ParseDuration(refreshInterval)
if err != nil {
log.Fatalf("invalid refresh interval: %v", err)
}
}
followLogs(opts, interval)
} else {
logs, err := api.AuditLogs(opts)
if err != nil {
log.Fatalf("failed to retrieve audit logs: %v", err)
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
log.WithField("output_format", formatFlag).Debug("Output format selected")
err = utils.PrintFormat(logs.Payload, formatFlag)
if err != nil {
return
}
} else {
list.ListLogs(logs.Payload)
}
}
},
}
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",
)
flags.BoolVarP(&follow, "follow", "f", false, "Follow log output (tail -f behavior)")
flags.StringVarP(&refreshInterval, "refresh-interval", "n", "",
"Interval to refresh logs when following (default: 5s)")
return cmd
}
func followLogs(opts api.ListFlags, interval time.Duration) {
var lastLogTime *time.Time
logsLogger.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
DisableColors: false,
})
logsLogger.SetLevel(log.InfoLevel)
logsLogger.SetOutput(os.Stdout)
fmt.Println("Following Harbor audit logs... (Press Ctrl+C to stop)")
for {
logs, err := api.AuditLogs(opts)
if err != nil {
log.Errorf("failed to retrieve audit logs: %v", err)
time.Sleep(interval)
continue
}
var newLogs []*models.AuditLogExt
if lastLogTime != nil {
for _, logEntry := range logs.Payload {
logTime := time.Time(logEntry.OpTime)
if !logTime.IsZero() && logTime.After(*lastLogTime) {
newLogs = append(newLogs, logEntry)
}
}
} else {
newLogs = logs.Payload
}
if len(logs.Payload) > 0 {
logTime := time.Time(logs.Payload[0].OpTime)
if !logTime.IsZero() {
lastLogTime = &logTime
}
}
printLogsAsStream(newLogs)
time.Sleep(interval)
}
}
func printLogsAsStream(logs []*models.AuditLogExt) {
for _, logEntry := range logs {
logTime := time.Time(logEntry.OpTime)
level := getLogLevel(logEntry.OperationResult)
displayUser := truncateUsername(logEntry.Username)
resource := getResourceInfo(logEntry.ResourceType, logEntry.Resource)
resultIcon := "✓"
if !logEntry.OperationResult {
resultIcon = "✗"
}
message := fmt.Sprintf("%s %s %s %s",
displayUser,
logEntry.Operation,
resource,
resultIcon)
entry := logsLogger.WithTime(logTime)
switch level {
case "error":
entry.Error(message)
case "info":
entry.Info(message)
default:
entry.Debug(message)
}
}
}
func truncateUsername(username string) string {
if username == "" {
return "unknown"
}
if len(username) > 30 {
if parts := strings.Split(username, "+"); len(parts) > 1 {
project := strings.TrimPrefix(parts[0], "robt_")
return fmt.Sprintf("%s+robot", project)
}
return username[:27] + "..."
}
return username
}
func getLogLevel(operationResult bool) string {
switch operationResult {
case false:
return "error"
case true:
return "info"
default:
return "error"
}
}
func getResourceInfo(resourceType, resource string) string {
if resourceType == "" && resource == "" {
return "unknown"
}
if resourceType != "" && resource != "" {
return fmt.Sprintf("%s:%s", resourceType, resource)
}
if resourceType != "" {
return resourceType
}
return resource
}

View File

@ -21,7 +21,7 @@ import (
"github.com/atotto/clipboard"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/config"
config "github.com/goharbor/harbor-cli/pkg/config/robot"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/robot/create"
@ -104,6 +104,9 @@ Examples:
}
logrus.Info("Successfully loaded robot configuration")
opts = *loadedOpts
if opts.ProjectName == "" {
opts.ProjectName = opts.Permissions[0].Namespace
}
permissions = make([]models.Permission, len(opts.Permissions[0].Access))
for i, access := range opts.Permissions[0].Access {
permissions[i] = models.Permission{
@ -113,7 +116,7 @@ Examples:
}
}
if opts.ProjectName == "" {
if opts.ProjectName == "" && configFile == "" {
opts.ProjectName, err = prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("%v", utils.ParseHarborErrorMsg(err))
@ -145,7 +148,7 @@ Examples:
}
permissions = choices
} else {
permissions = prompt.GetRobotPermissionsFromUser()
permissions = prompt.GetRobotPermissionsFromUser("project")
if len(permissions) == 0 {
msg := fmt.Errorf("no permissions selected, robot account needs at least one permission")
return fmt.Errorf("failed to create robot: %v", utils.ParseHarborErrorMsg(msg))
@ -166,6 +169,7 @@ Examples:
perm := &create.RobotPermission{
Namespace: opts.ProjectName,
Access: accesses,
Kind: "project", // Default to project level
}
opts.Permissions = []*create.RobotPermission{perm}
}
@ -180,7 +184,8 @@ Examples:
if exists {
return fmt.Errorf("robot account with name '%s' already exists in project '%s'", opts.Name, opts.ProjectName)
}
response, err := api.CreateRobot(opts, "project")
opts.Level = "project" // Default to project level
response, err := api.CreateRobot(opts)
if err != nil {
return fmt.Errorf("failed to create robot: %v", utils.ParseHarborErrorMsg(err))
}

View File

@ -104,11 +104,16 @@ Examples:
bot := robot.Payload
var duration int64
if bot.Duration != nil {
duration = *bot.Duration
}
opts = update.UpdateView{
CreationTime: bot.CreationTime,
Description: bot.Description,
Disable: bot.Disable,
Duration: bot.Duration,
Duration: duration,
Editable: bot.Editable,
ID: bot.ID,
Level: bot.Level,
@ -129,7 +134,7 @@ Examples:
}
permissions = choices
} else {
permissions = prompt.GetRobotPermissionsFromUser()
permissions = prompt.GetRobotPermissionsFromUser("project")
}
// []Permission to []*Access

View File

@ -45,7 +45,11 @@ func UpdateRegistryCommand() *cobra.Command {
registryId = prompt.GetRegistryNameFromUser()
}
existingRegistry := api.GetRegistryResponse(registryId)
existingRegistry, err := api.GetRegistryResponse(registryId)
if err != nil {
log.Errorf("failed to get registry with ID %d: %v", registryId, err)
return
}
if existingRegistry == nil {
log.Errorf("registry is not found")
return

View File

@ -22,10 +22,14 @@ func Replication() *cobra.Command {
var replicationCmd = &cobra.Command{
Use: "replication",
Aliases: []string{"repl"},
Short: "",
Long: ``,
Short: "Manage replications",
Long: `Manage replications in Harbor context`,
}
replicationCmd.AddCommand()
replicationCmd.AddCommand(
ReplicationPoliciesCommand(),
StartCommand(),
StopCommand(),
)
return replicationCmd
}

View File

@ -0,0 +1,38 @@
// 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 replication
import (
rpolicies "github.com/goharbor/harbor-cli/cmd/harbor/root/replication/policies"
"github.com/spf13/cobra"
)
func ReplicationPoliciesCommand() *cobra.Command {
// replicationCmd represents the replication command.
var replicationCmd = &cobra.Command{
Use: "policies",
Aliases: []string{"pol"},
Short: "Manage replication policies",
Long: `Manage replication policies in Harbor context`,
}
replicationCmd.AddCommand(
rpolicies.ListCommand(),
rpolicies.ViewCommand(),
rpolicies.DeleteCommand(),
rpolicies.CreateCommand(),
rpolicies.UpdateCommand(),
)
return replicationCmd
}

View File

@ -0,0 +1,185 @@
// 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 policies
import (
"fmt"
"strconv"
"strings"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/replication"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
config "github.com/goharbor/harbor-cli/pkg/config/replication"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/replication/policies/create"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func CreateCommand() *cobra.Command {
var configFile string
var registryID int64
var err error
var opts *create.CreateView
cmd := &cobra.Command{
Use: "create",
Short: "create replication policies",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug("Starting replications create command")
if configFile != "" {
log.Debugf("Loading replication policy configuration from file: %s", configFile)
opts, err = config.LoadConfigFromFile(configFile)
if err != nil {
return fmt.Errorf("failed to load replication policy configuration: %v", err)
}
registryID, err = api.GetRegistryIdByName(opts.TargetRegistry)
if err != nil {
return fmt.Errorf("failed to get registry ID for name %s: %v", opts.TargetRegistry, err)
}
if registryID == 0 {
return fmt.Errorf("registry with name %s not found", opts.TargetRegistry)
}
} else {
opts = &create.CreateView{}
create.CreateRPolicyView(opts, false)
registryID = prompt.GetRegistryNameFromUser()
}
registry, err := api.GetRegistryResponse(registryID)
if err != nil {
return fmt.Errorf("failed to get registry with ID %d: %v", registryID, err)
}
policy := ConvertToPolicy(opts, registry)
response, err := api.CreateReplicationPolicy(&replication.CreateReplicationPolicyParams{
Policy: policy,
})
if err != nil {
return fmt.Errorf("failed to create replication policy: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Println("Replication policy created successfully with ID:", response.Location)
return nil
},
}
flags := cmd.Flags()
flags.StringVarP(&configFile, "policy-config-file", "f", "", "YAML/JSON file with robot configuration")
return cmd
}
func ConvertToPolicy(view *create.CreateView, registry *models.Registry) *models.ReplicationPolicy {
policy := &models.ReplicationPolicy{
Name: view.Name,
Description: view.Description,
Enabled: view.Enabled,
Override: view.Override,
// ReplicateDeletion is the favored field to use for deletion replication
// Deletion is deprecated and will be removed in future versions
// However, for updating from false to true, we need to set both fields
ReplicateDeletion: view.ReplicateDeletion,
Deletion: view.ReplicateDeletion,
CopyByChunk: &view.CopyByChunk,
Filters: []*models.ReplicationFilter{},
}
if view.Speed != "" {
speedInt, _ := strconv.ParseInt(view.Speed, 10, 32)
speed := int32(speedInt)
policy.Speed = &speed
}
trigger := &models.ReplicationTrigger{
Type: view.TriggerType,
}
if view.TriggerType == "scheduled" {
trigger.TriggerSettings = &models.ReplicationTriggerSettings{
Cron: view.CronString,
}
}
policy.Trigger = trigger
if view.ReplicationMode == "Pull" {
// Pull mode (external -> Harbor)
policy.SrcRegistry = registry
policy.DestRegistry = nil
} else {
// Push mode (Harbor -> external)
policy.SrcRegistry = nil
policy.DestRegistry = registry
}
var resourceFilter *models.ReplicationFilter
var nameFilter *models.ReplicationFilter
var tagFilter *models.ReplicationFilter
// var labelFilter *models.ReplicationFilter
var filters []*models.ReplicationFilter
if view.ResourceFilter != "" {
resourceFilter = &models.ReplicationFilter{
Type: "resource",
Value: view.ResourceFilter,
Decoration: "",
}
filters = append(filters, resourceFilter)
}
if view.NameFilter != "" {
nameFilter = &models.ReplicationFilter{
Type: "name",
Value: view.NameFilter,
Decoration: "",
}
filters = append(filters, nameFilter)
}
if view.TagPattern != "" {
tagFilter = &models.ReplicationFilter{
Type: "tag",
Value: view.TagPattern,
Decoration: view.TagFilter,
}
filters = append(filters, tagFilter)
}
if view.LabelPattern != "" {
decoration := "matches"
if view.LabelFilter == "excludes" {
decoration = "excludes"
}
var labelValues []string
if strings.Contains(view.LabelPattern, ",") {
labelValues = strings.Split(view.LabelPattern, ",")
for i, label := range labelValues {
labelValues[i] = strings.TrimSpace(label)
}
} else {
labelValues = []string{strings.TrimSpace(view.LabelPattern)}
}
filters = append(filters, &models.ReplicationFilter{
Type: "label",
Value: labelValues,
Decoration: decoration,
})
}
policy.Filters = filters
return policy
}

View File

@ -0,0 +1,56 @@
// 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 policies
import (
"fmt"
"strconv"
"github.com/spf13/cobra"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
)
func DeleteCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete [NAME|ID]",
Short: "delete replication policy by name or id",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var rpolicyID int64
if len(args) > 0 {
var err error
// convert string to int64
rpolicyID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
}
} else {
rpolicyID = prompt.GetReplicationPolicyFromUser()
}
_, err := api.DeleteReplicationPolicy(rpolicyID)
if err != nil {
return fmt.Errorf("failed to get replication policy: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Printf("Replication policy %d deleted successfully\n", rpolicyID)
return nil
},
}
return cmd
}

View File

@ -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 policies
import (
"fmt"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/replication/policies/list"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func ListCommand() *cobra.Command {
var opts api.ListFlags
cmd := &cobra.Command{
Use: "list",
Short: "List replication policies",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug("Starting replications list command")
if opts.PageSize > 100 {
return fmt.Errorf("page size should be less than or equal to 100")
}
log.Debug("Fetching projects...")
allPolicies, err := api.ListReplicationPolicies(opts)
if err != nil {
return fmt.Errorf("failed to get projects list: %v", utils.ParseHarborErrorMsg(err))
}
log.WithField("count", len(allPolicies.Payload)).Debug("Number of projects fetched")
if len(allPolicies.Payload) == 0 {
log.Info("No policies found")
return nil
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
log.WithField("output_format", formatFlag).Debug("Output format selected")
err = utils.PrintFormat(allPolicies.Payload, formatFlag)
if err != nil {
return err
}
} else {
log.Debug("Listing projects using default view")
list.ListPolicies(allPolicies.Payload)
}
return nil
},
}
flags := cmd.Flags()
flags.Int64VarP(&opts.Page, "page", "", 1, "Page number")
flags.Int64VarP(&opts.PageSize, "page-size", "", 0, "Size of per page (0 to fetch all)")
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
}

View File

@ -0,0 +1,123 @@
// 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 policies
import (
"fmt"
"strconv"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/views/replication/policies/create"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// UpdateCommand returns a command to update existing replication policies
func UpdateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update [policy-id]",
Short: "Update an existing replication policy",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var policyID int64
if len(args) > 0 {
var err error
policyID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
}
} else {
policyID = prompt.GetReplicationPolicyFromUser()
}
existingPolicy, err := api.GetReplicationPolicy(policyID)
if err != nil {
return fmt.Errorf("failed to get replication policy: %w", err)
}
var existingReplicationMode string
if existingPolicy.Payload.SrcRegistry.ID != 0 && existingPolicy.Payload.DestRegistry.ID == 0 {
existingReplicationMode = "Pull"
} else if existingPolicy.Payload.SrcRegistry.ID == 0 && existingPolicy.Payload.DestRegistry.ID != 0 {
existingReplicationMode = "Push"
} else {
return fmt.Errorf("replication policy with ID %d is neither Pull nor Push", policyID)
}
createView := &create.CreateView{
Name: existingPolicy.Payload.Name,
Description: existingPolicy.Payload.Description,
Enabled: existingPolicy.Payload.Enabled,
Override: existingPolicy.Payload.Override,
ReplicateDeletion: existingPolicy.Payload.ReplicateDeletion,
ReplicationMode: existingReplicationMode,
}
if existingPolicy.Payload.CopyByChunk != nil {
createView.CopyByChunk = *existingPolicy.Payload.CopyByChunk
}
if existingPolicy.Payload.Speed != nil {
if *existingPolicy.Payload.Speed == 0 {
speed := int32(-1)
existingPolicy.Payload.Speed = &speed
}
createView.Speed = strconv.FormatInt(int64(*existingPolicy.Payload.Speed), 10)
}
if existingPolicy.Payload.SrcRegistry != nil && existingPolicy.Payload.DestRegistry == nil {
createView.ReplicationMode = "Pull"
} else if existingPolicy.Payload.SrcRegistry == nil && existingPolicy.Payload.DestRegistry != nil {
createView.ReplicationMode = "Push"
}
if existingPolicy.Payload.Trigger != nil {
createView.TriggerType = existingPolicy.Payload.Trigger.Type
if existingPolicy.Payload.Trigger.TriggerSettings != nil {
if existingPolicy.Payload.Trigger.Type == "scheduled" {
createView.CronString = existingPolicy.Payload.Trigger.TriggerSettings.Cron
} else if existingPolicy.Payload.Trigger.Type == "event_based" {
createView.ReplicateDeletion = existingPolicy.Payload.ReplicateDeletion
}
}
}
log.Infof("Updating replication policy: %s (ID: %d)", existingPolicy.Payload.Name, policyID)
create.CreateRPolicyView(createView, true)
var updatedPolicy *models.ReplicationPolicy
fmt.Println("Updated policy replicate deletion:", createView.ReplicateDeletion)
if createView.ReplicationMode == "Pull" {
updatedPolicy = ConvertToPolicy(createView, existingPolicy.Payload.SrcRegistry)
updatedPolicy.ID = policyID
} else {
updatedPolicy = ConvertToPolicy(createView, existingPolicy.Payload.DestRegistry)
}
_, err = api.UpdateReplicationPolicy(policyID, updatedPolicy)
if err != nil {
return fmt.Errorf("failed to update replication policy: %w", err)
}
log.Infof("Successfully updated replication policy: %s (ID: %d)", updatedPolicy.Name, policyID)
return nil
},
}
return cmd
}

View File

@ -0,0 +1,66 @@
// 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 policies
import (
"fmt"
"strconv"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
view "github.com/goharbor/harbor-cli/pkg/views/replication/policies/view"
)
func ViewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "view [NAME|ID]",
Short: "get replication policy by name or id",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var rpolicyID int64
if len(args) > 0 {
var err error
// convert string to int64
rpolicyID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
}
} else {
rpolicyID = prompt.GetReplicationPolicyFromUser()
}
response, err := api.GetReplicationPolicy(rpolicyID)
if err != nil {
return fmt.Errorf("failed to get replication policy: %v", utils.ParseHarborErrorMsg(err))
}
FormatFlag := viper.GetString("output-format")
if FormatFlag != "" {
err = utils.PrintFormat(response.Payload, FormatFlag)
if err != nil {
return err
}
} else {
view.ViewPolicy(response.Payload)
}
return nil
},
}
return cmd
}

View File

@ -0,0 +1,56 @@
// 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 replication
import (
"fmt"
"strconv"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func StartCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "start",
Short: "start replication",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug("Starting replication")
var rpolicyID int64
if len(args) > 0 {
var err error
// convert string to int64
rpolicyID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
}
} else {
rpolicyID = prompt.GetReplicationPolicyFromUser()
}
response, err := api.StartReplication(rpolicyID)
if err != nil {
return fmt.Errorf("failed to start replication: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Printf("Repliation started successfully with ID: %s\n", response.Location)
return nil
},
}
return cmd
}

View File

@ -0,0 +1,68 @@
// 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 replication
import (
"fmt"
"strconv"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func StopCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "stop",
Short: "stop replication",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug("Stopping replication")
var rpolicyID int64
var executionID int64
if len(args) > 0 {
var err error
// convert string to int64
rpolicyID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid replication policy ID: %s, %v", args[0], err)
}
executionID = prompt.GetReplicationExecutionIDFromUser(rpolicyID)
} else {
rpolicyID = prompt.GetReplicationPolicyFromUser()
executionID = prompt.GetReplicationExecutionIDFromUser(rpolicyID)
}
execution, err := api.GetReplicationExecution(executionID)
if err != nil {
return fmt.Errorf("failed to get replication execution: %v", utils.ParseHarborErrorMsg(err))
}
if execution.Payload.Status != "InProgress" {
return fmt.Errorf("replication execution with ID: %d is already stopped, succeed or failed", executionID)
}
_, err = api.StopReplication(executionID)
if err != nil {
return fmt.Errorf("failed to stop replication: %v", utils.ParseHarborErrorMsg(err))
}
fmt.Printf("Replication execution with ID: %d stopped successfully\n", executionID)
return nil
},
}
return cmd
}

View File

@ -0,0 +1,36 @@
// 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 robot
import (
"github.com/spf13/cobra"
)
func Robot() *cobra.Command {
cmd := &cobra.Command{
Use: "robot",
Short: "Manage robot accounts",
Example: ` harbor robot list`,
}
cmd.AddCommand(
ListRobotCommand(),
DeleteRobotCommand(),
ViewRobotCommand(),
CreateRobotCommand(),
UpdateRobotCommand(),
RefreshSecretCommand(),
)
return cmd
}

View File

@ -0,0 +1,439 @@
// 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 robot
import (
"encoding/json"
"fmt"
"os"
"github.com/atotto/clipboard"
"github.com/charmbracelet/huh"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
config "github.com/goharbor/harbor-cli/pkg/config/robot"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/robot/create"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func CreateRobotCommand() *cobra.Command {
var (
opts create.CreateView
all bool
exportToFile bool
configFile string
)
cmd := &cobra.Command{
Use: "create",
Short: "create robot",
Long: `Create a new robot account within Harbor.
Robot accounts are non-human users that can be used for automation purposes
such as CI/CD pipelines, scripts, or other automated processes that need
to interact with Harbor. They have specific permissions and a defined lifetime.
This command creates system-level robots that can have permissions spanning
multiple projects, making them suitable for automation tasks that need access
across your Harbor instance.
This command supports both interactive and non-interactive modes:
- Without flags: opens an interactive form for configuring the robot
- With flags: creates a robot with the specified parameters
- With config file: loads robot configuration from YAML or JSON
A robot account requires:
- A unique name
- A set of system permissions
- Optional project-specific permissions
- A duration (lifetime in days)
The generated robot credentials can be:
- Displayed on screen
- Copied to clipboard (default)
- Exported to a JSON file with the -e flag
Examples:
# Interactive mode
harbor-cli robot create
# Non-interactive mode with all flags
harbor-cli robot create --name ci-robot --description "CI pipeline" --duration 90
# Create with all permissions
harbor-cli robot create --name ci-robot --all-permission
# Load from configuration file
harbor-cli robot create --robot-config-file ./robot-config.yaml
# Export secret to file
harbor-cli robot create --name ci-robot --export-to-file`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
var permissions []models.Permission
var projectPermissionsMap = make(map[string][]models.Permission)
var accessesSystem []*models.Access
// Handle config file or interactive input
if configFile != "" {
if err := loadFromConfigFile(&opts, configFile, &permissions, projectPermissionsMap); err != nil {
return err
}
} else {
if err := handleInteractiveInput(&opts, all, &permissions, projectPermissionsMap); err != nil {
return err
}
}
// Build system access permissions
for _, perm := range permissions {
accessesSystem = append(accessesSystem, &models.Access{
Resource: perm.Resource,
Action: perm.Action,
})
}
// Build merged permissions structure
opts.Permissions = buildMergedPermissions(projectPermissionsMap, accessesSystem)
opts.Level = "system"
// Create robot and handle response
return createRobotAndHandleResponse(&opts, exportToFile)
},
}
addFlags(cmd, &opts, &all, &exportToFile, &configFile)
return cmd
}
func loadFromConfigFile(opts *create.CreateView, configFile string, permissions *[]models.Permission, projectPermissionsMap map[string][]models.Permission) error {
fmt.Println("Loading configuration from: ", configFile)
loadedOpts, err := config.LoadRobotConfigFromFile(configFile)
if err != nil {
return fmt.Errorf("failed to load robot config from file: %v", err)
}
logrus.Info("Successfully loaded robot configuration")
*opts = *loadedOpts
// Extract system-level and project permissions
var systemPermFound bool
for _, perm := range opts.Permissions {
if perm.Kind == "system" && perm.Namespace == "/" {
systemPermFound = true
*permissions = make([]models.Permission, len(perm.Access))
for i, access := range perm.Access {
(*permissions)[i] = models.Permission{
Resource: access.Resource,
Action: access.Action,
}
}
} else if perm.Kind == "project" {
var projectPerms []models.Permission
for _, access := range perm.Access {
projectPerms = append(projectPerms, models.Permission{
Resource: access.Resource,
Action: access.Action,
})
}
projectPermissionsMap[perm.Namespace] = projectPerms
}
}
if !systemPermFound {
return fmt.Errorf("system robot configuration must include system-level permissions")
}
logrus.Infof("Loaded system robot with %d system permissions and %d project-specific permissions",
len(*permissions), len(projectPermissionsMap))
return nil
}
func handleInteractiveInput(opts *create.CreateView, all bool, permissions *[]models.Permission, projectPermissionsMap map[string][]models.Permission) error {
// Show interactive form if needed
if opts.Name == "" || opts.Duration == 0 {
create.CreateRobotView(opts)
}
// Validate duration
if opts.Duration == 0 {
return fmt.Errorf("failed to create robot: %v", utils.ParseHarborErrorMsg(fmt.Errorf("duration cannot be 0")))
}
// Get system permissions
if err := getSystemPermissions(all, permissions); err != nil {
return err
}
// Get project permissions
return getProjectPermissions(opts, projectPermissionsMap)
}
func getSystemPermissions(all bool, permissions *[]models.Permission) error {
if len(*permissions) == 0 {
if all {
perms, _ := api.GetPermissions()
for _, perm := range perms.Payload.System {
*permissions = append(*permissions, *perm)
}
} else {
*permissions = prompt.GetRobotPermissionsFromUser("system")
if len(*permissions) == 0 {
return fmt.Errorf("failed to create robot: %v",
utils.ParseHarborErrorMsg(fmt.Errorf("no permissions selected, robot account needs at least one permission")))
}
}
}
return nil
}
func getProjectPermissions(opts *create.CreateView, projectPermissionsMap map[string][]models.Permission) error {
permissionMode, err := promptPermissionMode()
if err != nil {
return fmt.Errorf("error selecting permission mode: %v", err)
}
switch permissionMode {
case "list":
return handleMultipleProjectsPermissions(projectPermissionsMap)
case "per_project":
return handlePerProjectPermissions(opts, projectPermissionsMap)
case "none":
fmt.Println("Creating robot with system-level permissions only (no project-specific permissions)")
return nil
default:
return fmt.Errorf("unknown permission mode: %s", permissionMode)
}
}
func handleMultipleProjectsPermissions(projectPermissionsMap map[string][]models.Permission) error {
selectedProjects, err := getMultipleProjectsFromUser()
if err != nil {
return fmt.Errorf("error selecting projects: %v", err)
}
if len(selectedProjects) > 0 {
fmt.Println("Select permissions to apply to all selected projects:")
projectPermissions := prompt.GetRobotPermissionsFromUser("project")
for _, projectName := range selectedProjects {
projectPermissionsMap[projectName] = projectPermissions
}
}
return nil
}
func handlePerProjectPermissions(opts *create.CreateView, projectPermissionsMap map[string][]models.Permission) error {
if opts.ProjectName == "" {
for {
projectName, err := prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("%v", utils.ParseHarborErrorMsg(err))
}
if projectName == "" {
return fmt.Errorf("project name cannot be empty")
}
projectPermissionsMap[projectName] = prompt.GetRobotPermissionsFromUser("project")
moreProjects, err := promptMoreProjects()
if err != nil {
return fmt.Errorf("error asking for more projects: %v", err)
}
if !moreProjects {
break
}
}
} else {
projectPermissions := prompt.GetRobotPermissionsFromUser("project")
projectPermissionsMap[opts.ProjectName] = projectPermissions
}
return nil
}
func buildMergedPermissions(projectPermissionsMap map[string][]models.Permission, accessesSystem []*models.Access) []*create.RobotPermission {
var mergedPermissions []*create.RobotPermission
// Add project permissions
for projectName, projectPermissions := range projectPermissionsMap {
var accessesProject []*models.Access
for _, perm := range projectPermissions {
accessesProject = append(accessesProject, &models.Access{
Resource: perm.Resource,
Action: perm.Action,
})
}
mergedPermissions = append(mergedPermissions, &create.RobotPermission{
Namespace: projectName,
Access: accessesProject,
Kind: "project",
})
}
// Add system permissions
mergedPermissions = append(mergedPermissions, &create.RobotPermission{
Namespace: "/",
Access: accessesSystem,
Kind: "system",
})
return mergedPermissions
}
func createRobotAndHandleResponse(opts *create.CreateView, exportToFile bool) error {
response, err := api.CreateRobot(*opts)
if err != nil {
return fmt.Errorf("failed to create robot: %v", utils.ParseHarborErrorMsg(err))
}
logrus.Infof("Successfully created robot account '%s' (ID: %d)",
response.Payload.Name, response.Payload.ID)
// Handle output format
if formatFlag := viper.GetString("output-format"); formatFlag != "" {
res, _ := api.GetRobot(response.Payload.ID)
utils.SavePayloadJSON(response.Payload.Name, res.Payload)
return nil
}
// Handle secret output
name, secret := response.Payload.Name, response.Payload.Secret
if exportToFile {
logrus.Info("Exporting robot credentials to file")
exportSecretToFile(name, secret, response.Payload.CreationTime.String(), response.Payload.ExpiresAt)
return nil
}
create.CreateRobotSecretView(name, secret)
if err := clipboard.WriteAll(secret); err != nil {
logrus.Errorf("failed to write to clipboard")
} else {
fmt.Println("secret copied to clipboard.")
}
return nil
}
func addFlags(cmd *cobra.Command, opts *create.CreateView, all *bool, exportToFile *bool, configFile *string) {
flags := cmd.Flags()
flags.BoolVarP(all, "all-permission", "a", false, "Select all permissions for the robot account")
flags.BoolVarP(exportToFile, "export-to-file", "e", false, "Choose to export robot account to file")
flags.StringVarP(&opts.ProjectName, "project", "", "", "set project name")
flags.StringVarP(&opts.Name, "name", "", "", "name of the robot account")
flags.StringVarP(&opts.Description, "description", "", "", "description of the robot account")
flags.Int64VarP(&opts.Duration, "duration", "", 0, "set expiration of robot account in days")
flags.StringVarP(configFile, "robot-config-file", "r", "", "YAML/JSON file with robot configuration")
}
func exportSecretToFile(name, secret, creationTime string, expiresAt int64) {
secretJson := config.RobotSecret{
Name: name,
ExpiresAt: expiresAt,
CreationTime: creationTime,
Secret: secret,
}
filename := fmt.Sprintf("%s-secret.json", name)
jsonData, err := json.MarshalIndent(secretJson, "", " ")
if err != nil {
logrus.Errorf("Failed to marshal secret to JSON: %v", err)
return
}
if err := os.WriteFile(filename, jsonData, 0600); err != nil {
logrus.Errorf("Failed to write secret to file: %v", err)
return
}
fmt.Printf("Secret saved to %s\n", filename)
}
func getMultipleProjectsFromUser() ([]string, error) {
allProjects, err := api.ListAllProjects()
if err != nil {
return nil, fmt.Errorf("failed to list projects: %v", err)
}
var selectedProjects []string
var projectOptions []huh.Option[string]
for _, p := range allProjects.Payload {
projectOptions = append(projectOptions, huh.NewOption(p.Name, p.Name))
}
err = huh.NewForm(
huh.NewGroup(
huh.NewNote().
Title("Multiple Project Selection").
Description("Select the projects to assign the same permissions to this robot account."),
huh.NewMultiSelect[string]().
Title("Select projects").
Options(projectOptions...).
Value(&selectedProjects),
),
).WithTheme(huh.ThemeCharm()).WithWidth(80).Run()
return selectedProjects, err
}
func promptMoreProjects() (bool, error) {
var addMore bool
err := huh.NewForm(
huh.NewGroup(
huh.NewNote().
Title("Project Selection").
Description("You can add permissions for multiple projects to this robot account."),
huh.NewSelect[bool]().
Title("Do you want to select (more) projects?").
Description("Select 'Yes' to add (another) project, 'No' to continue with current selection.").
Options(
huh.NewOption("No", false),
huh.NewOption("Yes", true),
).
Value(&addMore),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).WithHeight(10).Run()
return addMore, err
}
func promptPermissionMode() (string, error) {
var permissionMode string
err := huh.NewForm(
huh.NewGroup(
huh.NewNote().
Title("Permission Mode").
Description("Select how you want to assign permissions to projects:"),
huh.NewSelect[string]().
Title("Permission Mode").
Description("Choose 'List' to select multiple projects with common permissions, or 'Per Project' for individual project permissions.").
Options(
huh.NewOption("No project permissions (system-level only)", "none"),
huh.NewOption("Per Project", "per_project"),
huh.NewOption("List", "list"),
).
Value(&permissionMode),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).WithHeight(10).Run()
return permissionMode, err
}

View File

@ -0,0 +1,80 @@
// 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 robot
import (
"fmt"
"strconv"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// to-do improve DeleteRobotCommand and multi select & delete
func DeleteRobotCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete [robotID]",
Short: "delete robot by id",
Long: `Delete a robot account from Harbor.
This command permanently removes a robot account from Harbor. Once deleted,
the robot's credentials will no longer be valid, and any automated processes
using those credentials will fail.
The command supports multiple ways to identify the robot account to delete:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for robot selection
Important considerations:
- Deletion is permanent and cannot be undone
- All access tokens for the robot will be invalidated immediately
- Any systems using the robot's credentials will need to be updated
- For system robots, access across all projects will be revoked
Examples:
# Delete robot by ID
harbor-cli robot delete 123
# Interactive deletion (will prompt for robot selection)
harbor-cli robot delete`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var (
robotID int64
err error
)
if len(args) == 1 {
robotID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
log.Fatalf("failed to parse robot ID: %v", err)
}
} else {
robotID = prompt.GetRobotIDFromUser(-1)
}
err = api.DeleteRobot(robotID)
if err != nil {
fmt.Printf("failed to delete robots: %v", utils.ParseHarborErrorMsg(err))
return
}
log.Infof("Successfully deleted robot with ID: %d", robotID)
fmt.Printf("Robot account (ID: %d) was successfully deleted\n", robotID)
},
}
return cmd
}

View File

@ -0,0 +1,95 @@
// 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 robot
import (
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/robot/list"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// ListRobotCommand creates a new `harbor project robot list` command
func ListRobotCommand() *cobra.Command {
var opts api.ListFlags
cmd := &cobra.Command{
Use: "list [projectName]",
Short: "list robot",
Long: `List robot accounts in Harbor.
This command displays a list of system-level robot accounts. The list includes basic
information about each robot account, such as ID, name, creation time, and
expiration status.
System-level robots have permissions that can span across multiple projects, making
them suitable for CI/CD pipelines and automation tasks that require access to
multiple projects in Harbor.
You can control the output using pagination flags and format options:
- Use --page and --page-size to navigate through results
- Use --sort to order the results by name, creation time, etc.
- Use -q/--query to filter robots by specific criteria
- Set output-format in your configuration for JSON, YAML, or other formats
Examples:
# List all system robots
harbor-cli robot list
# List system robots with pagination
harbor-cli robot list --page 2 --page-size 20
# List system robots with custom sorting
harbor-cli robot list --sort name
# Filter system robots by name
harbor-cli robot list -q name=ci-robot
# Get robot details in JSON format
harbor-cli robot list --output-format json`,
Args: cobra.MaximumNArgs(0),
Run: func(cmd *cobra.Command, args []string) {
robots, err := api.ListRobot(opts)
if err != nil {
log.Errorf("failed to get robots list: %v", utils.ParseHarborErrorMsg(err))
}
formatFlag := viper.GetString("output-format")
if formatFlag != "" {
err = utils.PrintFormat(robots, formatFlag)
if err != nil {
log.Errorf("Invalid Print Format: %v", err)
}
} else {
list.ListRobots(robots.Payload)
}
},
}
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
}

View File

@ -0,0 +1,135 @@
// 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 robot
import (
"fmt"
"strconv"
"github.com/atotto/clipboard"
"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/robot/create"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func RefreshSecretCommand() *cobra.Command {
var (
robotID int64
secret string
secretStdin bool
)
cmd := &cobra.Command{
Use: "refresh [robotID]",
Short: "refresh robot secret by id",
Long: `Refresh the secret for an existing robot account in Harbor.
This command generates a new secret for a robot account, effectively revoking
the old secret and requiring updates to any systems using the robot's credentials.
The command supports multiple ways to identify the robot account:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for both project and robot selection
You can specify the new secret in several ways:
- Let Harbor generate a random secret (default)
- Provide a custom secret with the --secret flag
- Pipe a secret via stdin using the --secret-stdin flag
After refreshing, the new secret will be:
- Displayed on screen
- Copied to clipboard for immediate use
- Usable immediately for authentication
Important considerations:
- The old secret will be invalidated immediately
- Any systems using the old credentials will need to be updated
- There is no way to recover the old secret after refreshing
Examples:
# Refresh robot secret by ID (generates a random secret)
harbor-cli project robot refresh 123
# Refresh with a custom secret
harbor-cli project robot refresh 123 --secret "MyCustomSecret123"
# Provide secret via stdin (useful for scripting)
echo "MySecretFromScript123" | harbor-cli project robot refresh 123 --secret-stdin
# Interactive refresh (will prompt for project and robot selection)
harbor-cli project robot refresh`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var err error
if len(args) == 1 {
robotID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
log.Fatalf("failed to parse robot ID: %v", err)
}
} else {
robotID = prompt.GetRobotIDFromUser(-1)
}
if secret != "" {
err = utils.ValidatePassword(secret)
if err != nil {
log.Fatalf("Invalid secret: %v\n", err)
}
}
if secretStdin {
secret = getSecret()
}
response, err := api.RefreshSecret(secret, robotID)
if err != nil {
log.Fatalf("failed to refresh robot secret: %v\n", err)
}
log.Info("Secret updated successfully.")
if response.Payload.Secret != "" {
secret = response.Payload.Secret
create.CreateRobotSecretView("", secret)
err = clipboard.WriteAll(response.Payload.Secret)
if err != nil {
log.Fatalf("failed to write the secret to the clipboard: %v", err)
}
fmt.Println("secret copied to clipboard.")
}
},
}
flags := cmd.Flags()
flags.StringVarP(&secret, "secret", "", "", "secret")
flags.BoolVarP(&secretStdin, "secret-stdin", "", false, "Take the robot secret from stdin")
return cmd
}
// getSecret from commandline
func getSecret() string {
secret, err := utils.GetSecretStdin("Enter your secret: ")
if err != nil {
log.Fatalf("Error reading secret: %v\n", err)
}
if err := utils.ValidatePassword(secret); err != nil {
log.Fatalf("Invalid secret: %v\n", err)
}
return secret
}

View File

@ -0,0 +1,625 @@
// 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 robot
import (
"fmt"
"strconv"
"github.com/charmbracelet/huh"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
config "github.com/goharbor/harbor-cli/pkg/config/robot"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/robot/update"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func UpdateRobotCommand() *cobra.Command {
var (
robotID int64
opts update.UpdateView
all bool
configFile string
)
cmd := &cobra.Command{
Use: "update [robotID]",
Short: "update robot by id",
Long: `Update an existing robot account within Harbor.
Robot accounts are non-human users that can be used for automation purposes
such as CI/CD pipelines, scripts, or other automated processes that need
to interact with Harbor. This command allows you to modify an existing robot's
properties including its name, description, duration, and permissions.
This command supports both interactive and non-interactive modes:
- With robot ID: directly updates the specified robot
- Without ID: walks through robot selection interactively
The update process will:
1. Identify the robot account to be updated
2. Load its current configuration
3. Apply the requested changes
4. Save the updated configuration
This command can update both system and project-specific permissions:
- System permissions apply across the entire Harbor instance
- Project permissions apply to specific projects
Configuration can be loaded from:
- Interactive prompts (default)
- Command line flags
- YAML/JSON configuration file
Note: Updating a robot does not regenerate its secret. If you need a new
secret, consider deleting the robot and creating a new one instead.
Examples:
# Update robot by ID with a new description
harbor-cli robot update 123 --description "Updated CI/CD pipeline robot"
# Update robot's duration (extend lifetime)
harbor-cli robot update 123 --duration 180
# Update with all permissions
harbor-cli robot update 123 --all-permission
# Update from configuration file
harbor-cli robot update 123 --robot-config-file ./robot-config.yaml
# Interactive update (will prompt for robot selection and changes)
harbor-cli robot update`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
// Get robot ID from args or interactive prompt
if len(args) == 1 {
robotID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("failed to parse robot ID: %v", err)
}
} else {
robotID = prompt.GetRobotIDFromUser(-1)
}
// Get current robot configuration
robot, err := api.GetRobot(robotID)
if err != nil {
return fmt.Errorf("failed to get robot: %v", utils.ParseHarborErrorMsg(err))
}
// Initialize update view with current values
bot := robot.Payload
opts.ID = bot.ID
opts.Level = bot.Level
opts.Name = bot.Name
opts.Secret = bot.Secret
opts.Description = bot.Description
opts.Duration = *bot.Duration
opts.Disable = bot.Disable
opts.Editable = bot.Editable
opts.CreationTime = bot.CreationTime
// Extract current permissions (both system and project)
var permissions []models.Permission
var projectPermissionsMap = make(map[string][]models.Permission)
// Separate system and project permissions
for _, perm := range bot.Permissions {
if perm.Kind == "system" && perm.Namespace == "/" {
for _, access := range perm.Access {
permissions = append(permissions, models.Permission{
Resource: access.Resource,
Action: access.Action,
})
}
} else if perm.Kind == "project" {
var projectPerms []models.Permission
for _, access := range perm.Access {
projectPerms = append(projectPerms, models.Permission{
Resource: access.Resource,
Action: access.Action,
})
}
projectPermissionsMap[perm.Namespace] = projectPerms
}
}
logrus.Infof("Loaded robot with %d system permissions and %d project-specific permissions",
len(permissions), len(projectPermissionsMap))
// Handle configuration from file or interactive input
if configFile != "" {
if err := loadFromConfigFileForUpdate(&opts, configFile, &permissions, projectPermissionsMap); err != nil {
return err
}
} else {
if err := handleInteractiveInputForUpdate(&opts, all, &permissions, projectPermissionsMap); err != nil {
return err
}
}
// Build system access permissions
var accessesSystem []*models.Access
for _, perm := range permissions {
accessesSystem = append(accessesSystem, &models.Access{
Resource: perm.Resource,
Action: perm.Action,
})
}
// Build merged permissions structure
opts.Permissions = buildMergedPermissionsForUpdate(projectPermissionsMap, accessesSystem)
// Update robot and handle response
return updateRobotAndHandleResponse(&opts)
},
}
addUpdateFlags(cmd, &opts, &all, &configFile)
return cmd
}
func loadFromConfigFileForUpdate(opts *update.UpdateView, configFile string, permissions *[]models.Permission, projectPermissionsMap map[string][]models.Permission) error {
fmt.Println("Loading configuration from: ", configFile)
loadedOpts, err := config.LoadRobotConfigFromFile(configFile)
if err != nil {
return fmt.Errorf("failed to load robot config from file: %v", err)
}
logrus.Info("Successfully loaded robot configuration")
// Only update fields that should be updated from the config file
// IMPORTANT: Do not update name or level as the Harbor API doesn't allow this
// if loadedOpts.Name != "" {
// opts.Name = loadedOpts.Name
// }
if loadedOpts.Description != "" {
opts.Description = loadedOpts.Description
}
if loadedOpts.Duration != 0 {
opts.Duration = loadedOpts.Duration
}
var systemPermFound bool
for _, perm := range loadedOpts.Permissions {
if perm.Kind == "system" && perm.Namespace == "/" {
systemPermFound = true
for _, access := range perm.Access {
*permissions = append(*permissions, models.Permission{
Resource: access.Resource,
Action: access.Action,
})
}
} else if perm.Kind == "project" {
var projectPerms []models.Permission
for _, access := range perm.Access {
projectPerms = append(projectPerms, models.Permission{
Resource: access.Resource,
Action: access.Action,
})
}
// Validate project permissions before adding
validProjectPerms, err := validateProjectPermissions(projectPerms)
if err != nil {
return err
}
projectPermissionsMap[perm.Namespace] = validProjectPerms
}
}
if !systemPermFound {
return fmt.Errorf("robot configuration must include system-level permissions")
}
logrus.Infof("Loaded robot update with %d system permissions and %d project-specific permissions",
len(*permissions), len(projectPermissionsMap))
return nil
}
func handleInteractiveInputForUpdate(opts *update.UpdateView, all bool, permissions *[]models.Permission, projectPermissionsMap map[string][]models.Permission) error {
// Show interactive form for updating basic details
update.UpdateRobotView(opts)
// Validate duration
if opts.Duration == 0 {
return fmt.Errorf("failed to update robot: %v", utils.ParseHarborErrorMsg(fmt.Errorf("duration cannot be 0")))
}
// Ask if user wants to update permissions
var updatePerms bool
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[bool]().
Title("Do you want to update permissions?").
Options(
huh.NewOption("No", false),
huh.NewOption("Yes", true),
).
Value(&updatePerms),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).Run()
if err != nil {
return fmt.Errorf("error asking about permission updates: %v", err)
}
if !updatePerms {
logrus.Info("Keeping existing permissions")
return nil
}
// Get system permissions
if err := getSystemPermissionsForUpdate(all, permissions); err != nil {
return err
}
// Get project permissions
return getProjectPermissionsForUpdate(opts, projectPermissionsMap)
}
func getSystemPermissionsForUpdate(all bool, permissions *[]models.Permission) error {
var updateSystem bool
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[bool]().
Title("Do you want to update system permissions?").
Options(
huh.NewOption("No", false),
huh.NewOption("Yes", true),
).
Value(&updateSystem),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).Run()
if err != nil {
return fmt.Errorf("error asking about system permission updates: %v", err)
}
if !updateSystem {
logrus.Info("Keeping existing system permissions")
return nil
}
if all {
perms, _ := api.GetPermissions()
*permissions = nil // Clear existing permissions
for _, perm := range perms.Payload.System {
*permissions = append(*permissions, *perm)
}
} else {
newPermissions := prompt.GetRobotPermissionsFromUser("system")
if len(newPermissions) == 0 {
return fmt.Errorf("failed to update robot: %v",
utils.ParseHarborErrorMsg(fmt.Errorf("no permissions selected, robot account needs at least one permission")))
}
*permissions = newPermissions
}
return nil
}
func getProjectPermissionsForUpdate(opts *update.UpdateView, projectPermissionsMap map[string][]models.Permission) error {
permissionMode, err := promptPermissionModeForUpdate(len(projectPermissionsMap) > 0)
if err != nil {
return fmt.Errorf("error selecting permission mode: %v", err)
}
switch permissionMode {
case "keep":
logrus.Info("Keeping existing project permissions")
return nil
case "clear":
logrus.Info("Clearing all project permissions")
// Clear the map to remove all project permissions
for k := range projectPermissionsMap {
delete(projectPermissionsMap, k)
}
return nil
case "list":
return handleMultipleProjectsPermissionsForUpdate(projectPermissionsMap)
case "per_project":
return handlePerProjectPermissionsForUpdate(projectPermissionsMap)
default:
return fmt.Errorf("unknown permission mode: %s", permissionMode)
}
}
func handleMultipleProjectsPermissionsForUpdate(projectPermissionsMap map[string][]models.Permission) error {
// First, decide whether to replace or keep existing project permissions
if len(projectPermissionsMap) > 0 {
var replaceExisting bool
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[bool]().
Title("What do you want to do with existing project permissions?").
Options(
huh.NewOption("Keep existing and add new", false),
huh.NewOption("Replace all existing with new selection", true),
).
Value(&replaceExisting),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).Run()
if err != nil {
return fmt.Errorf("error asking about existing permissions: %v", err)
}
if replaceExisting {
// Clear the map to remove all project permissions
for k := range projectPermissionsMap {
delete(projectPermissionsMap, k)
}
}
}
selectedProjects, err := getMultipleProjectsFromUser()
if err != nil {
return fmt.Errorf("error selecting projects: %v", err)
}
if len(selectedProjects) > 0 {
fmt.Println("Select permissions to apply to all selected projects:")
projectPermissions := prompt.GetRobotPermissionsFromUser("project")
// Validate project permissions
validProjectPerms, err := validateProjectPermissions(projectPermissions)
if err != nil {
return err
}
for _, projectName := range selectedProjects {
projectPermissionsMap[projectName] = validProjectPerms
}
}
return nil
}
func handlePerProjectPermissionsForUpdate(projectPermissionsMap map[string][]models.Permission) error {
// First, decide whether to replace or keep existing project permissions
if len(projectPermissionsMap) > 0 {
var modifyMode string
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("How do you want to modify project permissions?").
Options(
huh.NewOption("Add new projects only", "add"),
huh.NewOption("Modify existing projects", "modify"),
huh.NewOption("Replace all existing with new projects", "replace"),
).
Value(&modifyMode),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).Run()
if err != nil {
return fmt.Errorf("error asking about permission modification: %v", err)
}
if modifyMode == "replace" {
// Clear the map to remove all project permissions
for k := range projectPermissionsMap {
delete(projectPermissionsMap, k)
}
} else if modifyMode == "modify" {
// Show existing projects and let user select which to modify
var existingProjects []string
for project := range projectPermissionsMap {
existingProjects = append(existingProjects, project)
}
var selectedProjects []string
var projectOptions []huh.Option[string]
for _, p := range existingProjects {
projectOptions = append(projectOptions, huh.NewOption(p, p))
}
err = huh.NewForm(
huh.NewGroup(
huh.NewMultiSelect[string]().
Title("Select projects to modify").
Options(projectOptions...).
Value(&selectedProjects),
),
).WithTheme(huh.ThemeCharm()).WithWidth(80).Run()
if err != nil {
return fmt.Errorf("error selecting projects to modify: %v", err)
}
// Update permissions for selected projects
for _, project := range selectedProjects {
fmt.Printf("Updating permissions for project: %s\n", project)
projectPerms := prompt.GetRobotPermissionsFromUser("project")
// Validate project permissions
validProjectPerms, err := validateProjectPermissions(projectPerms)
if err != nil {
return err
}
projectPermissionsMap[project] = validProjectPerms
}
return nil
}
}
// Add new projects
for {
projectName, err := prompt.GetProjectNameFromUser()
if err != nil {
return fmt.Errorf("%v", utils.ParseHarborErrorMsg(err))
}
if projectName == "" {
return fmt.Errorf("project name cannot be empty")
}
projectPerms := prompt.GetRobotPermissionsFromUser("project")
// Validate project permissions
validProjectPerms, err := validateProjectPermissions(projectPerms)
if err != nil {
return err
}
projectPermissionsMap[projectName] = validProjectPerms
moreProjects, err := promptMoreProjects()
if err != nil {
return fmt.Errorf("error asking for more projects: %v", err)
}
if !moreProjects {
break
}
}
return nil
}
// validateProjectPermissions filters out permissions that are not valid for projects
func validateProjectPermissions(permissions []models.Permission) ([]models.Permission, error) {
perms, err := api.GetPermissions()
if err != nil {
return nil, fmt.Errorf("failed to get valid permissions: %v", err)
}
// Create a map of valid project permissions
validProjectPerms := make(map[string]bool)
for _, perm := range perms.Payload.Project {
key := fmt.Sprintf("%s:%s", perm.Resource, perm.Action)
validProjectPerms[key] = true
}
// Filter the permissions
var validPerms []models.Permission
var invalidPerms []string
for _, perm := range permissions {
key := fmt.Sprintf("%s:%s", perm.Resource, perm.Action)
if validProjectPerms[key] {
validPerms = append(validPerms, perm)
} else {
invalidPerms = append(invalidPerms, key)
}
}
// Warn about invalid permissions
if len(invalidPerms) > 0 {
logrus.Warnf("Removed %d invalid project permissions: %v", len(invalidPerms), invalidPerms)
}
return validPerms, nil
}
func buildMergedPermissionsForUpdate(projectPermissionsMap map[string][]models.Permission, accessesSystem []*models.Access) []*update.RobotPermission {
var mergedPermissions []*update.RobotPermission
// Add project permissions
for projectName, projectPermissions := range projectPermissionsMap {
var accessesProject []*models.Access
for _, perm := range projectPermissions {
accessesProject = append(accessesProject, &models.Access{
Resource: perm.Resource,
Action: perm.Action,
})
}
if len(accessesProject) > 0 {
mergedPermissions = append(mergedPermissions, &update.RobotPermission{
Namespace: projectName,
Access: accessesProject,
Kind: "project",
})
}
}
if len(accessesSystem) > 0 {
// Add system permissions only if there are any
mergedPermissions = append(mergedPermissions, &update.RobotPermission{
Namespace: "/",
Access: accessesSystem,
Kind: "system",
})
}
return mergedPermissions
}
func updateRobotAndHandleResponse(opts *update.UpdateView) error {
err := api.UpdateRobot(opts)
if err != nil {
return fmt.Errorf("failed to update robot: %v", utils.ParseHarborErrorMsg(err))
}
logrus.Infof("Successfully updated robot account '%s' (ID: %d)", opts.Name, opts.ID)
// Handle output format
if formatFlag := viper.GetString("output-format"); formatFlag != "" {
res, _ := api.GetRobot(opts.ID)
utils.SavePayloadJSON(opts.Name, res.Payload)
}
return nil
}
func addUpdateFlags(cmd *cobra.Command, opts *update.UpdateView, all *bool, configFile *string) {
flags := cmd.Flags()
flags.BoolVarP(all, "all-permission", "a", false, "Select all permissions for the robot account")
flags.StringVarP(&opts.Name, "name", "", "", "name of the robot account")
flags.StringVarP(&opts.Description, "description", "", "", "description of the robot account")
flags.Int64VarP(&opts.Duration, "duration", "", 0, "set expiration of robot account in days")
flags.StringVarP(configFile, "robot-config-file", "r", "", "YAML/JSON file with robot configuration")
}
func promptPermissionModeForUpdate(hasExistingProjectPerms bool) (string, error) {
var permissionMode string
var options []huh.Option[string]
if hasExistingProjectPerms {
options = []huh.Option[string]{
huh.NewOption("Keep existing project permissions", "keep"),
huh.NewOption("Clear all project permissions", "clear"),
huh.NewOption("Per Project (individual permissions)", "per_project"),
huh.NewOption("List (same permissions for multiple projects)", "list"),
}
} else {
options = []huh.Option[string]{
huh.NewOption("No project permissions (system-level only)", "clear"),
huh.NewOption("Per Project (individual permissions)", "per_project"),
huh.NewOption("List (same permissions for multiple projects)", "list"),
}
}
err := huh.NewForm(
huh.NewGroup(
huh.NewNote().
Title("Project Permission Mode").
Description("Select how you want to handle project permissions:"),
huh.NewSelect[string]().
Title("Permission Mode").
Options(options...).
Value(&permissionMode),
),
).WithTheme(huh.ThemeCharm()).WithWidth(60).WithHeight(10).Run()
return permissionMode, err
}

View File

@ -0,0 +1,87 @@
// 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 robot
import (
"strconv"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/robot"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/prompt"
"github.com/goharbor/harbor-cli/pkg/views/robot/view"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func ViewRobotCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "view [robotID]",
Short: "get robot by id",
Long: `View detailed information about a robot account in Harbor.
This command displays comprehensive information about a robot account including
its ID, name, description, creation time, expiration, and the permissions
it has been granted. Supports both system-level and project-level robot accounts.
The command supports multiple ways to identify the robot account:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for robot selection
The displayed information includes:
- Basic details (ID, name, description)
- Temporal information (creation date, expiration date, remaining time)
- Security details (disabled status)
- Detailed permissions breakdown by resource and action
- For system robots: permissions across multiple projects are shown separately
System-level robots can have permissions spanning multiple projects, while
project-level robots are scoped to a single project.
Examples:
# View robot by ID
harbor-cli robot view 123
# Interactive selection (will prompt for robot)
harbor-cli robot view`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var (
robot *robot.GetRobotByIDOK
robotID int64
err error
)
if len(args) == 1 {
robotID, err = strconv.ParseInt(args[0], 10, 64)
if err != nil {
log.Fatalf("failed to parse robot ID: %v", err)
}
} else {
robotID = prompt.GetRobotIDFromUser(-1)
}
robot, err = api.GetRobot(robotID)
if err != nil {
log.Fatalf("failed to get robot: %v", err)
}
// Convert to a list and display
// robots := &models.Robot{robot.Payload}
view.ViewRobot(robot.Payload)
},
}
return cmd
}

View File

@ -0,0 +1,49 @@
---
title: harbor logs
weight: 30
---
## harbor logs
### Description
##### Get recent logs of the projects which the user is a member of
### Synopsis
Get recent logs of the projects which the user is a member of.
This command retrieves the audit logs for the projects the user is a member of. It supports pagination, sorting, and filtering through query parameters. The logs can be followed in real-time with the --follow flag, and the output can be formatted as JSON with the --output-format flag.
harbor-cli logs --page 1 --page-size 10 --query "operation=push" --sort "op_time:desc"
harbor-cli logs --follow --refresh-interval 2s
harbor-cli logs --output-format json
```sh
harbor logs [flags]
```
### Options
```sh
-f, --follow Follow log output (tail -f behavior)
-h, --help help for logs
--page int Page number (default 1)
--page-size int Size of per page (default 10)
-q, --query string Query string to query resources
-n, --refresh-interval string Interval to refresh logs when following (default: 5s)
--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
-v, --verbose verbose output
```
### SEE ALSO
* [harbor](harbor.md) - Official Harbor CLI

View File

@ -0,0 +1,33 @@
---
title: harbor replication policies create
weight: 85
---
## harbor replication policies create
### Description
##### create replication policies
```sh
harbor replication policies create [flags]
```
### Options
```sh
-h, --help help for create
-f, --policy-config-file string YAML/JSON file with robot configuration
```
### 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 replication policies](harbor-replication-policies.md) - Manage replication policies

View File

@ -0,0 +1,32 @@
---
title: harbor replication policies delete
weight: 55
---
## harbor replication policies delete
### Description
##### delete replication policy by name or id
```sh
harbor replication policies delete [NAME|ID] [flags]
```
### Options
```sh
-h, --help help for delete
```
### 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 replication policies](harbor-replication-policies.md) - Manage replication policies

View File

@ -0,0 +1,36 @@
---
title: harbor replication policies list
weight: 85
---
## harbor replication policies list
### Description
##### List replication policies
```sh
harbor replication policies list [flags]
```
### Options
```sh
-h, --help help for list
--page int Page number (default 1)
--page-size int Size of per page (0 to fetch all)
-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
-v, --verbose verbose output
```
### SEE ALSO
* [harbor replication policies](harbor-replication-policies.md) - Manage replication policies

View File

@ -0,0 +1,32 @@
---
title: harbor replication policies update
weight: 5
---
## harbor replication policies update
### Description
##### Update an existing replication policy
```sh
harbor replication policies update [policy-id] [flags]
```
### Options
```sh
-h, --help help for update
```
### 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 replication policies](harbor-replication-policies.md) - Manage replication policies

View File

@ -0,0 +1,32 @@
---
title: harbor replication policies view
weight: 20
---
## harbor replication policies view
### Description
##### get replication policy by name or id
```sh
harbor replication policies view [NAME|ID] [flags]
```
### Options
```sh
-h, --help help for view
```
### 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 replication policies](harbor-replication-policies.md) - Manage replication policies

View File

@ -0,0 +1,37 @@
---
title: harbor replication policies
weight: 50
---
## harbor replication policies
### Description
##### Manage replication policies
### Synopsis
Manage replication policies in Harbor context
### Options
```sh
-h, --help help for policies
```
### 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 replication](harbor-replication.md) - Manage replications
* [harbor replication policies create](harbor-replication-policies-create.md) - create replication policies
* [harbor replication policies delete](harbor-replication-policies-delete.md) - delete replication policy by name or id
* [harbor replication policies list](harbor-replication-policies-list.md) - List replication policies
* [harbor replication policies update](harbor-replication-policies-update.md) - Update an existing replication policy
* [harbor replication policies view](harbor-replication-policies-view.md) - get replication policy by name or id

View File

@ -0,0 +1,32 @@
---
title: harbor replication start
weight: 35
---
## harbor replication start
### Description
##### start replication
```sh
harbor replication start [flags]
```
### Options
```sh
-h, --help help for start
```
### 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 replication](harbor-replication.md) - Manage replications

View File

@ -0,0 +1,32 @@
---
title: harbor replication stop
weight: 70
---
## harbor replication stop
### Description
##### stop replication
```sh
harbor replication stop [flags]
```
### Options
```sh
-h, --help help for stop
```
### 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 replication](harbor-replication.md) - Manage replications

View File

@ -0,0 +1,35 @@
---
title: harbor replication
weight: 5
---
## harbor replication
### Description
##### Manage replications
### Synopsis
Manage replications in Harbor context
### Options
```sh
-h, --help help for replication
```
### 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 replication policies](harbor-replication-policies.md) - Manage replication policies
* [harbor replication start](harbor-replication-start.md) - start replication
* [harbor replication stop](harbor-replication-stop.md) - stop replication

View File

@ -0,0 +1,83 @@
---
title: harbor robot create
weight: 15
---
## harbor robot create
### Description
##### create robot
### Synopsis
Create a new robot account within Harbor.
Robot accounts are non-human users that can be used for automation purposes
such as CI/CD pipelines, scripts, or other automated processes that need
to interact with Harbor. They have specific permissions and a defined lifetime.
This command creates system-level robots that can have permissions spanning
multiple projects, making them suitable for automation tasks that need access
across your Harbor instance.
This command supports both interactive and non-interactive modes:
- Without flags: opens an interactive form for configuring the robot
- With flags: creates a robot with the specified parameters
- With config file: loads robot configuration from YAML or JSON
A robot account requires:
- A unique name
- A set of system permissions
- Optional project-specific permissions
- A duration (lifetime in days)
The generated robot credentials can be:
- Displayed on screen
- Copied to clipboard (default)
- Exported to a JSON file with the -e flag
Examples:
# Interactive mode
harbor-cli robot create
# Non-interactive mode with all flags
harbor-cli robot create --name ci-robot --description "CI pipeline" --duration 90
# Create with all permissions
harbor-cli robot create --name ci-robot --all-permission
# Load from configuration file
harbor-cli robot create --robot-config-file ./robot-config.yaml
# Export secret to file
harbor-cli robot create --name ci-robot --export-to-file
```sh
harbor robot create [flags]
```
### Options
```sh
-a, --all-permission Select all permissions for the robot account
--description string description of the robot account
--duration int set expiration of robot account in days
-e, --export-to-file Choose to export robot account to file
-h, --help help for create
--name string name of the robot account
--project string set project name
-r, --robot-config-file string YAML/JSON file with robot configuration
```
### 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 robot](harbor-robot.md) - Manage robot accounts

View File

@ -0,0 +1,57 @@
---
title: harbor robot delete
weight: 10
---
## harbor robot delete
### Description
##### delete robot by id
### Synopsis
Delete a robot account from Harbor.
This command permanently removes a robot account from Harbor. Once deleted,
the robot's credentials will no longer be valid, and any automated processes
using those credentials will fail.
The command supports multiple ways to identify the robot account to delete:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for robot selection
Important considerations:
- Deletion is permanent and cannot be undone
- All access tokens for the robot will be invalidated immediately
- Any systems using the robot's credentials will need to be updated
- For system robots, access across all projects will be revoked
Examples:
# Delete robot by ID
harbor-cli robot delete 123
# Interactive deletion (will prompt for robot selection)
harbor-cli robot delete
```sh
harbor robot delete [robotID] [flags]
```
### Options
```sh
-h, --help help for delete
```
### 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 robot](harbor-robot.md) - Manage robot accounts

View File

@ -0,0 +1,70 @@
---
title: harbor robot list
weight: 65
---
## harbor robot list
### Description
##### list robot
### Synopsis
List robot accounts in Harbor.
This command displays a list of system-level robot accounts. The list includes basic
information about each robot account, such as ID, name, creation time, and
expiration status.
System-level robots have permissions that can span across multiple projects, making
them suitable for CI/CD pipelines and automation tasks that require access to
multiple projects in Harbor.
You can control the output using pagination flags and format options:
- Use --page and --page-size to navigate through results
- Use --sort to order the results by name, creation time, etc.
- Use -q/--query to filter robots by specific criteria
- Set output-format in your configuration for JSON, YAML, or other formats
Examples:
# List all system robots
harbor-cli robot list
# List system robots with pagination
harbor-cli robot list --page 2 --page-size 20
# List system robots with custom sorting
harbor-cli robot list --sort name
# Filter system robots by name
harbor-cli robot list -q name=ci-robot
# Get robot details in JSON format
harbor-cli robot list --output-format json
```sh
harbor robot list [projectName] [flags]
```
### Options
```sh
-h, --help help for list
--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
-v, --verbose verbose output
```
### SEE ALSO
* [harbor robot](harbor-robot.md) - Manage robot accounts

View File

@ -0,0 +1,73 @@
---
title: harbor robot refresh
weight: 40
---
## harbor robot refresh
### Description
##### refresh robot secret by id
### Synopsis
Refresh the secret for an existing robot account in Harbor.
This command generates a new secret for a robot account, effectively revoking
the old secret and requiring updates to any systems using the robot's credentials.
The command supports multiple ways to identify the robot account:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for both project and robot selection
You can specify the new secret in several ways:
- Let Harbor generate a random secret (default)
- Provide a custom secret with the --secret flag
- Pipe a secret via stdin using the --secret-stdin flag
After refreshing, the new secret will be:
- Displayed on screen
- Copied to clipboard for immediate use
- Usable immediately for authentication
Important considerations:
- The old secret will be invalidated immediately
- Any systems using the old credentials will need to be updated
- There is no way to recover the old secret after refreshing
Examples:
# Refresh robot secret by ID (generates a random secret)
harbor-cli project robot refresh 123
# Refresh with a custom secret
harbor-cli project robot refresh 123 --secret "MyCustomSecret123"
# Provide secret via stdin (useful for scripting)
echo "MySecretFromScript123" | harbor-cli project robot refresh 123 --secret-stdin
# Interactive refresh (will prompt for project and robot selection)
harbor-cli project robot refresh
```sh
harbor robot refresh [robotID] [flags]
```
### Options
```sh
-h, --help help for refresh
--secret string secret
--secret-stdin Take the robot secret from stdin
```
### 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 robot](harbor-robot.md) - Manage robot accounts

View File

@ -0,0 +1,84 @@
---
title: harbor robot update
weight: 40
---
## harbor robot update
### Description
##### update robot by id
### Synopsis
Update an existing robot account within Harbor.
Robot accounts are non-human users that can be used for automation purposes
such as CI/CD pipelines, scripts, or other automated processes that need
to interact with Harbor. This command allows you to modify an existing robot's
properties including its name, description, duration, and permissions.
This command supports both interactive and non-interactive modes:
- With robot ID: directly updates the specified robot
- Without ID: walks through robot selection interactively
The update process will:
1. Identify the robot account to be updated
2. Load its current configuration
3. Apply the requested changes
4. Save the updated configuration
This command can update both system and project-specific permissions:
- System permissions apply across the entire Harbor instance
- Project permissions apply to specific projects
Configuration can be loaded from:
- Interactive prompts (default)
- Command line flags
- YAML/JSON configuration file
Note: Updating a robot does not regenerate its secret. If you need a new
secret, consider deleting the robot and creating a new one instead.
Examples:
# Update robot by ID with a new description
harbor-cli robot update 123 --description "Updated CI/CD pipeline robot"
# Update robot's duration (extend lifetime)
harbor-cli robot update 123 --duration 180
# Update with all permissions
harbor-cli robot update 123 --all-permission
# Update from configuration file
harbor-cli robot update 123 --robot-config-file ./robot-config.yaml
# Interactive update (will prompt for robot selection and changes)
harbor-cli robot update
```sh
harbor robot update [robotID] [flags]
```
### Options
```sh
-a, --all-permission Select all permissions for the robot account
--description string description of the robot account
--duration int set expiration of robot account in days
-h, --help help for update
--name string name of the robot account
-r, --robot-config-file string YAML/JSON file with robot configuration
```
### 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 robot](harbor-robot.md) - Manage robot accounts

View File

@ -0,0 +1,61 @@
---
title: harbor robot view
weight: 25
---
## harbor robot view
### Description
##### get robot by id
### Synopsis
View detailed information about a robot account in Harbor.
This command displays comprehensive information about a robot account including
its ID, name, description, creation time, expiration, and the permissions
it has been granted. Supports both system-level and project-level robot accounts.
The command supports multiple ways to identify the robot account:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for robot selection
The displayed information includes:
- Basic details (ID, name, description)
- Temporal information (creation date, expiration date, remaining time)
- Security details (disabled status)
- Detailed permissions breakdown by resource and action
- For system robots: permissions across multiple projects are shown separately
System-level robots can have permissions spanning multiple projects, while
project-level robots are scoped to a single project.
Examples:
# View robot by ID
harbor-cli robot view 123
# Interactive selection (will prompt for robot)
harbor-cli robot view
```sh
harbor robot view [robotID] [flags]
```
### Options
```sh
-h, --help help for view
```
### 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 robot](harbor-robot.md) - Manage robot accounts

View File

@ -0,0 +1,40 @@
---
title: harbor robot
weight: 75
---
## harbor robot
### Description
##### Manage robot accounts
### Examples
```sh
harbor robot list
```
### Options
```sh
-h, --help help for robot
```
### 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 robot create](harbor-robot-create.md) - create robot
* [harbor robot delete](harbor-robot-delete.md) - delete robot by id
* [harbor robot list](harbor-robot-list.md) - list robot
* [harbor robot refresh](harbor-robot-refresh.md) - refresh robot secret by id
* [harbor robot update](harbor-robot-update.md) - update robot by id
* [harbor robot view](harbor-robot-view.md) - get robot by id

View File

@ -43,10 +43,13 @@ harbor help
* [harbor instance](harbor-instance.md) - Manage preheat provider instances in Harbor
* [harbor label](harbor-label.md) - Manage labels in Harbor
* [harbor login](harbor-login.md) - Log in to Harbor registry
* [harbor logs](harbor-logs.md) - Get recent logs of the projects which the user is a member of
* [harbor project](harbor-project.md) - Manage projects and assign resources to them
* [harbor quota](harbor-quota.md) - Manage quotas
* [harbor registry](harbor-registry.md) - Manage registries
* [harbor replication](harbor-replication.md) - Manage replications
* [harbor repo](harbor-repo.md) - Manage repositories
* [harbor robot](harbor-robot.md) - Manage robot accounts
* [harbor scan-all](harbor-scan-all.md) - Scan all artifacts
* [harbor scanner](harbor-scanner.md) - scanner commands
* [harbor schedule](harbor-schedule.md) - Schedule jobs in Harbor

View File

@ -50,10 +50,10 @@ func Doc() error {
folderName := "cli-docs"
_, err = os.Stat(folderName)
if os.IsNotExist(err) {
log.Println("kumar err bhaa")
log.Printf("Folder %s does not exist", folderName)
err = os.Mkdir(folderName, 0755)
if err != nil {
log.Println("kumar err bhaa noooo")
log.Printf("Failed to create directory %s : %v", folderName, err)
log.Fatal("Error creating folder:", err)
}
}

View File

@ -0,0 +1,69 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-logs - Get recent logs of the projects which the user is a member of
.SH SYNOPSIS
\fBharbor logs [flags]\fP
.SH DESCRIPTION
Get recent logs of the projects which the user is a member of.
This command retrieves the audit logs for the projects the user is a member of. It supports pagination, sorting, and filtering through query parameters. The logs can be followed in real-time with the --follow flag, and the output can be formatted as JSON with the --output-format flag.
.PP
harbor-cli logs --page 1 --page-size 10 --query "operation=push" --sort "op_time:desc"
.PP
harbor-cli logs --follow --refresh-interval 2s
.PP
harbor-cli logs --output-format json
.SH OPTIONS
\fB-f\fP, \fB--follow\fP[=false]
Follow log output (tail -f behavior)
.PP
\fB-h\fP, \fB--help\fP[=false]
help for logs
.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-n\fP, \fB--refresh-interval\fP=""
Interval to refresh logs when following (default: 5s)
.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
.PP
\fB-v\fP, \fB--verbose\fP[=false]
verbose output
.SH SEE ALSO
\fBharbor(1)\fP

View File

@ -0,0 +1,39 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-replication-policies-create - create replication policies
.SH SYNOPSIS
\fBharbor replication policies create [flags]\fP
.SH DESCRIPTION
create replication policies
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for create
.PP
\fB-f\fP, \fB--policy-config-file\fP=""
YAML/JSON file with robot configuration
.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-replication-policies(1)\fP

View File

@ -0,0 +1,35 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-replication-policies-delete - delete replication policy by name or id
.SH SYNOPSIS
\fBharbor replication policies delete [NAME|ID] [flags]\fP
.SH DESCRIPTION
delete replication policy by name or id
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for delete
.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-replication-policies(1)\fP

View File

@ -0,0 +1,51 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-replication-policies-list - List replication policies
.SH SYNOPSIS
\fBharbor replication policies list [flags]\fP
.SH DESCRIPTION
List replication policies
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for list
.PP
\fB--page\fP=1
Page number
.PP
\fB--page-size\fP=0
Size of per page (0 to fetch all)
.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
.PP
\fB-v\fP, \fB--verbose\fP[=false]
verbose output
.SH SEE ALSO
\fBharbor-replication-policies(1)\fP

View File

@ -0,0 +1,35 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-replication-policies-update - Update an existing replication policy
.SH SYNOPSIS
\fBharbor replication policies update [policy-id] [flags]\fP
.SH DESCRIPTION
Update an existing replication policy
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for update
.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-replication-policies(1)\fP

View File

@ -0,0 +1,35 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-replication-policies-view - get replication policy by name or id
.SH SYNOPSIS
\fBharbor replication policies view [NAME|ID] [flags]\fP
.SH DESCRIPTION
get replication policy by name or id
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for view
.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-replication-policies(1)\fP

View File

@ -0,0 +1,35 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-replication-policies - Manage replication policies
.SH SYNOPSIS
\fBharbor replication policies [flags]\fP
.SH DESCRIPTION
Manage replication policies in Harbor context
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for policies
.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-replication(1)\fP, \fBharbor-replication-policies-create(1)\fP, \fBharbor-replication-policies-delete(1)\fP, \fBharbor-replication-policies-list(1)\fP, \fBharbor-replication-policies-update(1)\fP, \fBharbor-replication-policies-view(1)\fP

View File

@ -0,0 +1,35 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-replication-start - start replication
.SH SYNOPSIS
\fBharbor replication start [flags]\fP
.SH DESCRIPTION
start replication
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for start
.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-replication(1)\fP

View File

@ -0,0 +1,35 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-replication-stop - stop replication
.SH SYNOPSIS
\fBharbor replication stop [flags]\fP
.SH DESCRIPTION
stop replication
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for stop
.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-replication(1)\fP

View File

@ -0,0 +1,35 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-replication - Manage replications
.SH SYNOPSIS
\fBharbor replication [flags]\fP
.SH DESCRIPTION
Manage replications in Harbor context
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for replication
.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-replication-policies(1)\fP, \fBharbor-replication-start(1)\fP, \fBharbor-replication-stop(1)\fP

View File

@ -0,0 +1,113 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-robot-create - create robot
.SH SYNOPSIS
\fBharbor robot create [flags]\fP
.SH DESCRIPTION
Create a new robot account within Harbor.
.PP
Robot accounts are non-human users that can be used for automation purposes
such as CI/CD pipelines, scripts, or other automated processes that need
to interact with Harbor. They have specific permissions and a defined lifetime.
.PP
This command creates system-level robots that can have permissions spanning
multiple projects, making them suitable for automation tasks that need access
across your Harbor instance.
.PP
This command supports both interactive and non-interactive modes:
- Without flags: opens an interactive form for configuring the robot
- With flags: creates a robot with the specified parameters
- With config file: loads robot configuration from YAML or JSON
.PP
A robot account requires:
- A unique name
- A set of system permissions
- Optional project-specific permissions
- A duration (lifetime in days)
.PP
The generated robot credentials can be:
- Displayed on screen
- Copied to clipboard (default)
- Exported to a JSON file with the -e flag
.PP
Examples:
# Interactive mode
harbor-cli robot create
.PP
# Non-interactive mode with all flags
harbor-cli robot create --name ci-robot --description "CI pipeline" --duration 90
.PP
# Create with all permissions
harbor-cli robot create --name ci-robot --all-permission
.PP
# Load from configuration file
harbor-cli robot create --robot-config-file ./robot-config.yaml
.PP
# Export secret to file
harbor-cli robot create --name ci-robot --export-to-file
.SH OPTIONS
\fB-a\fP, \fB--all-permission\fP[=false]
Select all permissions for the robot account
.PP
\fB--description\fP=""
description of the robot account
.PP
\fB--duration\fP=0
set expiration of robot account in days
.PP
\fB-e\fP, \fB--export-to-file\fP[=false]
Choose to export robot account to file
.PP
\fB-h\fP, \fB--help\fP[=false]
help for create
.PP
\fB--name\fP=""
name of the robot account
.PP
\fB--project\fP=""
set project name
.PP
\fB-r\fP, \fB--robot-config-file\fP=""
YAML/JSON file with robot configuration
.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-robot(1)\fP

View File

@ -0,0 +1,61 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-robot-delete - delete robot by id
.SH SYNOPSIS
\fBharbor robot delete [robotID] [flags]\fP
.SH DESCRIPTION
Delete a robot account from Harbor.
.PP
This command permanently removes a robot account from Harbor. Once deleted,
the robot's credentials will no longer be valid, and any automated processes
using those credentials will fail.
.PP
The command supports multiple ways to identify the robot account to delete:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for robot selection
.PP
Important considerations:
- Deletion is permanent and cannot be undone
- All access tokens for the robot will be invalidated immediately
- Any systems using the robot's credentials will need to be updated
- For system robots, access across all projects will be revoked
.PP
Examples:
# Delete robot by ID
harbor-cli robot delete 123
.PP
# Interactive deletion (will prompt for robot selection)
harbor-cli robot delete
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for delete
.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-robot(1)\fP

View File

@ -0,0 +1,89 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-robot-list - list robot
.SH SYNOPSIS
\fBharbor robot list [projectName] [flags]\fP
.SH DESCRIPTION
List robot accounts in Harbor.
.PP
This command displays a list of system-level robot accounts. The list includes basic
information about each robot account, such as ID, name, creation time, and
expiration status.
.PP
System-level robots have permissions that can span across multiple projects, making
them suitable for CI/CD pipelines and automation tasks that require access to
multiple projects in Harbor.
.PP
You can control the output using pagination flags and format options:
- Use --page and --page-size to navigate through results
- Use --sort to order the results by name, creation time, etc.
- Use -q/--query to filter robots by specific criteria
- Set output-format in your configuration for JSON, YAML, or other formats
.PP
Examples:
# List all system robots
harbor-cli robot list
.PP
# List system robots with pagination
harbor-cli robot list --page 2 --page-size 20
.PP
# List system robots with custom sorting
harbor-cli robot list --sort name
.PP
# Filter system robots by name
harbor-cli robot list -q name=ci-robot
.PP
# Get robot details in JSON format
harbor-cli robot list --output-format json
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for list
.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
.PP
\fB-v\fP, \fB--verbose\fP[=false]
verbose output
.SH SEE ALSO
\fBharbor-robot(1)\fP

View File

@ -0,0 +1,87 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-robot-refresh - refresh robot secret by id
.SH SYNOPSIS
\fBharbor robot refresh [robotID] [flags]\fP
.SH DESCRIPTION
Refresh the secret for an existing robot account in Harbor.
.PP
This command generates a new secret for a robot account, effectively revoking
the old secret and requiring updates to any systems using the robot's credentials.
.PP
The command supports multiple ways to identify the robot account:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for both project and robot selection
.PP
You can specify the new secret in several ways:
- Let Harbor generate a random secret (default)
- Provide a custom secret with the --secret flag
- Pipe a secret via stdin using the --secret-stdin flag
.PP
After refreshing, the new secret will be:
- Displayed on screen
- Copied to clipboard for immediate use
- Usable immediately for authentication
.PP
Important considerations:
- The old secret will be invalidated immediately
- Any systems using the old credentials will need to be updated
- There is no way to recover the old secret after refreshing
.PP
Examples:
# Refresh robot secret by ID (generates a random secret)
harbor-cli project robot refresh 123
.PP
# Refresh with a custom secret
harbor-cli project robot refresh 123 --secret "MyCustomSecret123"
.PP
# Provide secret via stdin (useful for scripting)
echo "MySecretFromScript123" | harbor-cli project robot refresh 123 --secret-stdin
.PP
# Interactive refresh (will prompt for project and robot selection)
harbor-cli project robot refresh
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for refresh
.PP
\fB--secret\fP=""
secret
.PP
\fB--secret-stdin\fP[=false]
Take the robot secret from stdin
.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-robot(1)\fP

View File

@ -0,0 +1,109 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-robot-update - update robot by id
.SH SYNOPSIS
\fBharbor robot update [robotID] [flags]\fP
.SH DESCRIPTION
Update an existing robot account within Harbor.
.PP
Robot accounts are non-human users that can be used for automation purposes
such as CI/CD pipelines, scripts, or other automated processes that need
to interact with Harbor. This command allows you to modify an existing robot's
properties including its name, description, duration, and permissions.
.PP
This command supports both interactive and non-interactive modes:
- With robot ID: directly updates the specified robot
- Without ID: walks through robot selection interactively
.PP
The update process will:
1. Identify the robot account to be updated
2. Load its current configuration
3. Apply the requested changes
4. Save the updated configuration
.PP
This command can update both system and project-specific permissions:
- System permissions apply across the entire Harbor instance
- Project permissions apply to specific projects
.PP
Configuration can be loaded from:
- Interactive prompts (default)
- Command line flags
- YAML/JSON configuration file
.PP
Note: Updating a robot does not regenerate its secret. If you need a new
secret, consider deleting the robot and creating a new one instead.
.PP
Examples:
# Update robot by ID with a new description
harbor-cli robot update 123 --description "Updated CI/CD pipeline robot"
.PP
# Update robot's duration (extend lifetime)
harbor-cli robot update 123 --duration 180
.PP
# Update with all permissions
harbor-cli robot update 123 --all-permission
.PP
# Update from configuration file
harbor-cli robot update 123 --robot-config-file ./robot-config.yaml
.PP
# Interactive update (will prompt for robot selection and changes)
harbor-cli robot update
.SH OPTIONS
\fB-a\fP, \fB--all-permission\fP[=false]
Select all permissions for the robot account
.PP
\fB--description\fP=""
description of the robot account
.PP
\fB--duration\fP=0
set expiration of robot account in days
.PP
\fB-h\fP, \fB--help\fP[=false]
help for update
.PP
\fB--name\fP=""
name of the robot account
.PP
\fB-r\fP, \fB--robot-config-file\fP=""
YAML/JSON file with robot configuration
.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-robot(1)\fP

View File

@ -0,0 +1,66 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-robot-view - get robot by id
.SH SYNOPSIS
\fBharbor robot view [robotID] [flags]\fP
.SH DESCRIPTION
View detailed information about a robot account in Harbor.
.PP
This command displays comprehensive information about a robot account including
its ID, name, description, creation time, expiration, and the permissions
it has been granted. Supports both system-level and project-level robot accounts.
.PP
The command supports multiple ways to identify the robot account:
- By providing the robot ID directly as an argument
- Without any arguments, which will prompt for robot selection
.PP
The displayed information includes:
- Basic details (ID, name, description)
- Temporal information (creation date, expiration date, remaining time)
- Security details (disabled status)
- Detailed permissions breakdown by resource and action
- For system robots: permissions across multiple projects are shown separately
.PP
System-level robots can have permissions spanning multiple projects, while
project-level robots are scoped to a single project.
.PP
Examples:
# View robot by ID
harbor-cli robot view 123
.PP
# Interactive selection (will prompt for robot)
harbor-cli robot view
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for view
.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-robot(1)\fP

View File

@ -0,0 +1,41 @@
.nh
.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals"
.SH NAME
harbor-robot - Manage robot accounts
.SH SYNOPSIS
\fBharbor robot [flags]\fP
.SH DESCRIPTION
Manage robot accounts
.SH OPTIONS
\fB-h\fP, \fB--help\fP[=false]
help for robot
.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 robot list
.EE
.SH SEE ALSO
\fBharbor(1)\fP, \fBharbor-robot-create(1)\fP, \fBharbor-robot-delete(1)\fP, \fBharbor-robot-list(1)\fP, \fBharbor-robot-refresh(1)\fP, \fBharbor-robot-update(1)\fP, \fBharbor-robot-view(1)\fP

View File

@ -43,4 +43,4 @@ harbor help
.SH SEE ALSO
\fBharbor-artifact(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-login(1)\fP, \fBharbor-project(1)\fP, \fBharbor-quota(1)\fP, \fBharbor-registry(1)\fP, \fBharbor-repo(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-webhook(1)\fP
\fBharbor-artifact(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-login(1)\fP, \fBharbor-logs(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-webhook(1)\fP

View File

@ -0,0 +1,260 @@
# Harbor CLI Replication Policy Configuration
This guide explains how to use YAML or JSON configuration files to create Harbor replication policies with the Harbor CLI.
## Overview
Instead of using interactive prompts, you can define replication policies in configuration files and apply them using the `--policy-config-file` or `-f` flag.
## Usage
```bash
# Using YAML file
./harbor-cli replication policies create -f policy.yaml
# Using JSON file
./harbor-cli replication policies create -f policy.json
```
## Configuration File Structure
### Basic Policy Configuration
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✅ | Unique name for the replication policy |
| `description` | string | ❌ | Optional description of the policy |
| `replication_mode` | string | ✅ | `"push"` or `"pull"` |
| `target_registry` | string | ✅ | Name of the target registry |
| `trigger_mode` | string | ✅ | `"manual"`, `"scheduled"`, or `"event_based"` |
| `cron_string` | string | ❌ | Required for scheduled triggers (6-field cron format) |
| `bandwidth_limit` | string | ❌ | Bandwidth limit in KB/s (`"-1"` for unlimited) |
| `override` | boolean | ❌ | Replace existing artifacts (default: `false`) |
| `replicate_deletion` | boolean | ❌ | Sync deletion operations (default: `false`) |
| `copy_by_chunk` | boolean | ❌ | Transfer in chunks (default: `false`) |
| `enabled` | boolean | ❌ | Enable policy after creation (default: `false`) |
### Replication Filters
The `replication_filter` array contains filter objects that determine which artifacts to replicate.
#### Filter Types
| Type | Description | Decoration Support | Valid Values |
|------|-------------|-------------------|--------------|
| `resource` | Artifact type | ❌ | `"image"` |
| `name` | Repository name pattern | ❌ | Any string with wildcards (`*`) |
| `tag` | Tag pattern | ✅ | Any string with wildcards (`*`) |
| `label` | Label key=value pattern | ✅ | `"key=value"` format |
#### Decoration Values
- `"matches"` - Include artifacts matching the pattern
- `"excludes"` - Exclude artifacts matching the pattern
**Note:** Only `tag` and `label` filters support decoration. `resource` and `name` filters always match.
## Examples
### 1. Manual Pull Policy (YAML)
```yaml
# manual-pull.yaml
name: "manual-pull-from-dockerhub"
description: "Manually pull specific nginx images from Docker Hub"
replication_mode: "pull"
target_registry: "dockerhub-proxy"
trigger_mode: "manual"
bandwidth_limit: "-1"
override: false
replicate_deletion: false
enabled: true
copy_by_chunk: false
replication_filter:
- type: "resource"
value: "image"
- type: "name"
value: "nginx"
- type: "tag"
decoration: "matches"
value: "1.*"
- type: "label"
decoration: "matches"
value: "maintainer=NGINX Docker Maintainers"
```
### 2. Scheduled Push Policy (JSON)
```json
{
"name": "nightly-backup-push",
"description": "Push all production images to backup registry every night",
"replication_mode": "push",
"target_registry": "backup-harbor",
"trigger_mode": "scheduled",
"cron_string": "0 0 2 * * *",
"bandwidth_limit": "2048",
"override": true,
"replicate_deletion": true,
"enabled": true,
"copy_by_chunk": true,
"replication_filter": [
{
"type": "resource",
"value": "image"
},
{
"type": "name",
"value": "production/*"
},
{
"type": "tag",
"decoration": "excludes",
"value": "*-snapshot"
},
{
"type": "tag",
"decoration": "excludes",
"value": "*-dev"
},
{
"type": "label",
"decoration": "matches",
"value": "release=stable"
}
]
}
```
### 3. Event-Based Push Policy (YAML)
```yaml
# event-based.yaml
name: "realtime-sync"
description: "Automatically sync artifacts to partner registry on push"
replication_mode: "push"
target_registry: "partner-registry"
trigger_mode: "event_based"
bandwidth_limit: "512"
override: true
replicate_deletion: true
enabled: true
copy_by_chunk: false
replication_filter:
- type: "name"
value: "apps/*"
- type: "tag"
decoration: "matches"
value: "[0-9]*.[0-9]*.[0-9]*"
- type: "label"
decoration: "excludes"
value: "stage=development"
- type: "label"
decoration: "matches"
value: "public=true"
```
## Trigger Modes
### Manual
- Replication runs only when manually triggered
- No additional configuration required
### Scheduled
- Replication runs on a cron schedule
- Requires `cron_string` field with 6-field format: `"seconds minutes hours day-month month day-week"`
- Example: `"0 0 2 * * *"` (daily at 2:00 AM)
### Event-Based
- Replication runs automatically when artifacts are pushed/updated
- Only available for `"push"` mode
- Supports `replicate_deletion` for syncing deletions
## Common Patterns
### Filter by Repository Namespace
```yaml
replication_filter:
- type: "name"
value: "library/*" # All repositories in 'library' namespace
```
### Version-Specific Filtering
```yaml
replication_filter:
- type: "tag"
decoration: "matches"
value: "v*" # Only tags starting with 'v'
- type: "tag"
decoration: "excludes"
value: "*-rc*" # Exclude release candidates
```
### Label-Based Filtering
```yaml
replication_filter:
- type: "label"
decoration: "matches"
value: "environment=production"
- type: "label"
decoration: "excludes"
value: "experimental=true"
```
## Validation Rules
- Policy `name` must be unique and non-empty
- `replication_mode` must be `"push"` or `"pull"`
- `trigger_mode` must be `"manual"`, `"scheduled"`, or `"event_based"`
- `cron_string` is required for scheduled triggers
- `event_based` triggers are only available for push mode
- Resource filter value must be `"image"` or `"artifact"` (if specified)
- Tag and label filters support decoration, others don't
## File Formats
Both YAML and JSON formats are supported:
- `.yaml` or `.yml` files are parsed as YAML
- `.json` files are parsed as JSON
- File extension is required
## Error Handling
Common validation errors:
- `name is required` - Missing policy name
- `replication_mode must be 'push' or 'pull'` - Invalid replication mode
- `decoration is only supported for 'tag' and 'label' filters` - Invalid decoration usage
- `resource value must be 'image'` - Invalid resource filter value
- `cron string cannot be empty for scheduled trigger` - Missing cron string
## Best Practices
1. **Use descriptive names** - Policy names should clearly indicate their purpose
2. **Start with manual triggers** - Test policies manually before automating
3. **Set bandwidth limits** - Prevent replication from overwhelming network
4. **Use specific filters** - Avoid replicating unnecessary artifacts
5. **Enable deletion sync carefully** - Only use when you want true synchronization
6. **Test with small datasets** - Validate filters with limited scope first
## Registry Configuration
Before using replication policies, ensure your target registries are configured in Harbor:
```bash
# List available registries
./harbor-cli registry list
# Create a new registry if needed
./harbor-cli registry create
```
The `target_registry` field in your configuration must match an existing registry name in Harbor.

View File

@ -0,0 +1,37 @@
{
"name": "realtime-chart-sync",
"description": "Automatically sync Helm charts to partner registry on push",
"replication_mode": "push",
"target_registry": "dockerhub",
"trigger_mode": "event_based",
"bandwidth_limit": "512",
"override": true,
"replicate_deletion": true,
"enabled": true,
"copy_by_chunk": false,
"replication_filter": [
{
"type": "resource",
"value": "artifact"
},
{
"type": "name",
"value": "apps/*"
},
{
"type": "tag",
"decoration": "matches",
"value": "[0-9]*.[0-9]*.[0-9]*"
},
{
"type": "label",
"decoration": "excludes",
"value": "stage=development"
},
{
"type": "label",
"decoration": "matches",
"value": "public=true"
}
]
}

View File

@ -0,0 +1,30 @@
# Event-based push replication policy
name: "realtime-chart-sync"
description: "Automatically sync Helm charts to partner registry on push"
replication_mode: "push"
target_registry: "dockerhub"
trigger_mode: "event_based"
bandwidth_limit: "512" # 512 KB/s
override: true
replicate_deletion: true
enabled: true
copy_by_chunk: false
replication_filter:
- type: "resource"
value: "artifact"
- type: "name"
value: "apps/*"
- type: "tag"
decoration: "matches"
value: "[0-9]*.[0-9]*.[0-9]*" # Semantic versioning only
- type: "label"
decoration: "excludes"
value: "stage=development"
- type: "label"
decoration: "matches"
value: "public=true"

View File

@ -0,0 +1,32 @@
{
"name": "manual-pull-from-dockerhub",
"description": "Manually pull specific nginx images from Docker Hub",
"replication_mode": "pull",
"target_registry": "dockerhub",
"trigger_mode": "manual",
"bandwidth_limit": "-1",
"override": false,
"replicate_deletion": false,
"enabled": true,
"copy_by_chunk": false,
"replication_filter": [
{
"type": "resource",
"value": "image"
},
{
"type": "name",
"value": "nginx"
},
{
"type": "tag",
"decoration": "matches",
"value": "1.*"
},
{
"type": "label",
"decoration": "matches",
"value": "maintainer=NGINX Docker Maintainers"
}
]
}

View File

@ -0,0 +1,26 @@
# Manual pull replication policy
name: "manual-pull-from-dockerhub"
description: "Manually pull specific nginx images from Docker Hub"
replication_mode: "pull"
target_registry: "dockerhub"
trigger_mode: "manual"
bandwidth_limit: "-1" # Unlimited
override: false
replicate_deletion: false
enabled: true
copy_by_chunk: false
replication_filter:
- type: "resource"
value: "image"
- type: "name"
value: "nginx"
- type: "tag"
decoration: "matches"
value: "1.*"
- type: "label"
decoration: "matches"
value: "maintainer=NGINX Docker Maintainers"

View File

@ -0,0 +1,38 @@
{
"name": "nightly-backup-push",
"description": "Push all production images to backup registry every night",
"replication_mode": "push",
"target_registry": "dockerhub",
"trigger_mode": "scheduled",
"cron_string": "0 0 2 * * *",
"bandwidth_limit": "2048",
"override": true,
"replicate_deletion": true,
"enabled": true,
"copy_by_chunk": true,
"replication_filter": [
{
"type": "resource",
"value": "image"
},
{
"type": "name",
"value": "production/*"
},
{
"type": "tag",
"decoration": "excludes",
"value": "*-snapshot"
},
{
"type": "tag",
"decoration": "excludes",
"value": "*-dev"
},
{
"type": "label",
"decoration": "matches",
"value": "release=stable"
}
]
}

View File

@ -0,0 +1,31 @@
# Scheduled push replication policy
name: "nightly-backup-push"
description: "Push all production images to backup registry every night"
replication_mode: "push"
target_registry: "dockerhub"
trigger_mode: "scheduled"
cron_string: "0 0 2 * * *" # Every day at 2 AM
bandwidth_limit: "2048" # 2MB/s
override: true
replicate_deletion: true
enabled: true
copy_by_chunk: true
replication_filter:
- type: "resource"
value: "image"
- type: "name"
value: "production/*"
- type: "tag"
decoration: "excludes"
value: "*-snapshot"
- type: "tag"
decoration: "excludes"
value: "*-dev"
- type: "label"
decoration: "matches"
value: "release=stable"

View File

@ -7,20 +7,23 @@ This document describes the **YAML** format used to declare robot accounts, thei
## 1. File Schema
```yaml
name: "robot-name" # Required  unique name for the robot account
description: "..." # Optional  freeform description
duration: 90 # Required  lifetime in days (1 means no expiration)
project: "project-name" # Required  target project
permissions: # Required  at least one rule (see below)
# Either a single resource …
- resource: "repository"
actions: ["pull", "push"]
# … or multiple resources that share the same actions
- resources: ["artifact", "scan"]
actions: ["read"]
# Grant every action for a single resource with '*'
- resource: "project"
actions: ["*"]
name: "robot-name" # Required: Name of the robot account
description: "..." # Optional: Description of the robot account
duration: 90 # Required: Lifetime in days (-1 for no expiration)
kind: "project" # Required: "project" or "system" - determines robot type
permissions: # Required: Permission scopes
- access: # List of access items within this scope
- resource: "repository" # Either specify a single resource
actions:
- "pull"
- "push"
- resources: # Or specify multiple resources
- "artifact"
- "scan"
actions:
- "read"
kind: "project" # Permission scope kind (project or system)
namespace: "my-project" # Project name for project scope, "/" for system scope
```
**Key rules**
@ -30,7 +33,7 @@ permissions: # Required  at least one rule (see below)
| `name` | *string* | Must be unique per project. Lowercase letters, numbers, and dashes recommended. |
| `description` | *string* | Optional but **highly encouraged** for auditability. |
| `duration` | *integer* | Days until the robot expires. Use `1` for an unlimited lifetime. Values `0` or < `1` are rejected. |
| `project` | *string* | The Harbor project where the robot lives. |
| `kind` | *string* | "project" or "system" level robot. |
| `permissions` | *list* | One or more permission blocks, each granting one **set of actions** to one or more **resources**. |
> \*\*Tip \*\*: Store separate YAML files per robot to keep Git history clean and roll back permission changes safely.
@ -79,10 +82,13 @@ A permission block has three fields:
name: "ci-pipeline-robot"
description: "Robot account for CI/CD pipeline"
duration: 90
project: "my-project"
kind: "project"
permissions:
- resource: "repository"
actions: ["pull", "push"]
- access:
- resource: "repository"
actions: ["pull", "push"]
kind: "project"
namespace: "my-project"
```
### 3.2 ReadOnly Monitoring Robot (No Expiration)
@ -90,11 +96,14 @@ permissions:
```yaml
name: "read-only-robot"
description: "Read-only access for monitoring"
kind: "project"
duration: -1
project: "my-project"
permissions:
- resources: ["repository", "artifact", "scan"]
actions: ["read", "list"]
- access:
- resources: ["repository", "artifact", "scan"]
actions: ["read", "list"]
kind: "project"
namespace: "my-project"
```
### 3.3 Project Admin Robot (Full Access)
@ -102,11 +111,14 @@ permissions:
```yaml
name: "project-admin-robot"
description: "Project administration tasks"
kind: "project"
duration: 180
project: "my-project"
permissions:
- resource: "project"
actions: ["*"]
- access:
- resource: "project"
actions: ["*"]
kind: "project"
namespace: "my-project"
```
---

View File

@ -0,0 +1,30 @@
{
"name": "ci-pipeline-robot",
"description": "Robot account for CI/CD pipeline",
"duration": 90,
"level": "project",
"permissions": [
{
"access": [
{
"resource": "repository",
"actions": [
"pull",
"push"
]
},
{
"resources": [
"artifact",
"scan"
],
"actions": [
"read"
]
}
],
"kind": "project",
"namespace": "demo"
}
]
}

View File

@ -0,0 +1,17 @@
name: ci-pipeline-robot
description: "Robot account for CI/CD pipeline"
duration: 90
level: project
permissions:
- access:
- resource: repository
actions:
- pull
- push
- resources:
- artifact
- scan
actions:
- read
kind: project
namespace: demo

View File

@ -1,30 +0,0 @@
{
"name": "ci-pipeline-robot",
"description": "Robot account for CI/CD pipeline",
"duration": 90,
"project": "demo",
"permissions": [
{
"resource": "repository",
"actions": [
"pull",
"push"
]
},
{
"resources": [
"artifact",
"scan"
],
"actions": [
"read"
]
},
{
"resource": "project",
"actions": [
"*"
]
}
]
}

View File

@ -1,17 +0,0 @@
name: ci-pipeline-robot
description: "Robot account for CI/CD pipeline"
duration: 90
project: demo
permissions:
- resource: repository
actions:
- pull
- push
- resources:
- artifact
- scan
actions:
- read
- resource: project
actions:
- "*"

View File

@ -0,0 +1,44 @@
{
"name": "ci-pipeline-robot",
"description": "Robot account for CI/CD pipeline",
"duration": 90,
"level": "system",
"permissions": [
{
"access": [
{
"resource": "repository",
"actions": [
"pull",
"push"
]
},
{
"resources": [
"artifact",
"scan"
],
"actions": [
"read"
]
}
],
"kind": "project",
"namespace": "demo"
},
{
"access": [
{
"resources": [
"*"
],
"actions": [
"*"
]
}
],
"kind": "system",
"namespace": "/"
}
]
}

View File

@ -0,0 +1,22 @@
name: ci-pipeline-robot
description: "Robot account for CI/CD pipeline"
duration: 90
level: system
permissions:
- access:
- resource: repository
actions:
- pull
- push
- resources:
- artifact
- scan
actions:
- read
kind: project
namespace: demo
- access:
- resources: ["*"]
actions: ["*"]
kind: system
namespace: /

14
go.mod
View File

@ -5,7 +5,7 @@ go 1.24
require (
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.5
github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/huh v0.7.0
github.com/charmbracelet/lipgloss v1.1.0
github.com/sirupsen/logrus v1.9.3
@ -13,7 +13,7 @@ require (
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/zalando/go-keyring v0.2.6
golang.org/x/term v0.32.0
golang.org/x/term v0.33.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
)
@ -23,7 +23,7 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20241222104055-e1130b311607 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
@ -58,7 +58,7 @@ require (
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sync v0.16.0 // indirect
)
require (
@ -75,7 +75,7 @@ require (
github.com/go-openapi/strfmt v0.23.0
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/goharbor/go-client v0.210.0
github.com/goharbor/go-client v0.213.1
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
@ -88,8 +88,8 @@ require (
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0
)
replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88

28
go.sum
View File

@ -14,16 +14,16 @@ github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc=
github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
@ -87,8 +87,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/goharbor/go-client v0.210.0 h1:QwgLcWNSC3MFhBe7lq3BxDPtKQiD3k6hf6Lt26NChOI=
github.com/goharbor/go-client v0.210.0/go.mod h1:XMWHucuHU9VTRx6U6wYwbRuyCVhE6ffJGRjaeo0nvwo=
github.com/goharbor/go-client v0.213.1 h1:bohLwNog8uv8FKhIZ0SHiaDbYr3X/1hovgo5fqZWMdo=
github.com/goharbor/go-client v0.213.1/go.mod h1:XMWHucuHU9VTRx6U6wYwbRuyCVhE6ffJGRjaeo0nvwo=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
@ -188,17 +188,17 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

Binary file not shown.

View File

@ -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 api
import (
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/auditlog"
"github.com/goharbor/harbor-cli/pkg/utils"
)
func AuditLogs(opts ListFlags) (*auditlog.ListAuditLogExtsOK, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
}
response, err := client.Auditlog.ListAuditLogExts(ctx,
&auditlog.ListAuditLogExtsParams{
Q: &opts.Q,
Sort: &opts.Sort,
Page: &opts.Page,
PageSize: &opts.PageSize,
})
if err != nil {
return nil, err
}
return response, nil
}
func AuditLogsLegacy(opts ListFlags) (*auditlog.ListAuditLogsOK, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
}
response, err := client.Auditlog.ListAuditLogs(ctx,
&auditlog.ListAuditLogsParams{
Q: &opts.Q,
Sort: &opts.Sort,
Page: &opts.Page,
PageSize: &opts.PageSize,
})
if err != nil {
return nil, err
}
return response, nil
}
func AuditLogEventTypes() (*auditlog.ListAuditLogEventTypesOK, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
}
response, err := client.Auditlog.ListAuditLogEventTypes(ctx,
&auditlog.ListAuditLogEventTypesParams{})
if err != nil {
return nil, err
}
return response, nil
}

View File

@ -113,20 +113,20 @@ func ViewRegistry(registryId int64) (*registry.GetRegistryOK, error) {
return response, nil
}
func GetRegistryResponse(registryId int64) *models.Registry {
func GetRegistryResponse(registryId int64) (*models.Registry, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil
return nil, err
}
response, err := client.Registry.GetRegistry(ctx, &registry.GetRegistryParams{ID: registryId})
if err != nil {
return nil
return nil, err
}
if response.Payload.ID == 0 {
return nil
return nil, err
}
return response.GetPayload()
return response.GetPayload(), err
}
func UpdateRegistry(updateView *models.Registry, projectID int64) error {

View File

@ -12,3 +12,160 @@
// 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/replication"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/utils"
)
func ListReplicationPolicies(opts ...ListFlags) (*replication.ListReplicationPoliciesOK, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
}
var listFlags ListFlags
if len(opts) > 0 {
listFlags = opts[0]
}
response, err := client.Replication.ListReplicationPolicies(ctx, &replication.ListReplicationPoliciesParams{
Page: &listFlags.Page,
PageSize: &listFlags.PageSize,
Q: &listFlags.Q,
Name: &listFlags.Name,
Sort: &listFlags.Sort,
})
if err != nil {
return nil, err
}
return response, nil
}
func GetReplicationPolicy(policyID int64) (*replication.GetReplicationPolicyOK, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
}
response, err := client.Replication.GetReplicationPolicy(ctx, &replication.GetReplicationPolicyParams{ID: policyID})
if err != nil {
return nil, err
}
return response, nil
}
func DeleteReplicationPolicy(policyID int64) (*replication.DeleteReplicationPolicyOK, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
}
response, err := client.Replication.DeleteReplicationPolicy(ctx, &replication.DeleteReplicationPolicyParams{ID: policyID})
if err != nil {
return nil, err
}
return response, nil
}
func CreateReplicationPolicy(policy *replication.CreateReplicationPolicyParams) (*replication.CreateReplicationPolicyCreated, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
}
response, err := client.Replication.CreateReplicationPolicy(ctx, policy)
if err != nil {
return nil, err
}
return response, nil
}
func UpdateReplicationPolicy(policyID int64, policy *models.ReplicationPolicy) (*replication.UpdateReplicationPolicyOK, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
}
response, err := client.Replication.UpdateReplicationPolicy(ctx, &replication.UpdateReplicationPolicyParams{
ID: policyID,
Policy: policy,
})
if err != nil {
return nil, err
}
return response, nil
}
func StartReplication(policyID int64) (*replication.StartReplicationCreated, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
}
response, err := client.Replication.StartReplication(ctx, &replication.StartReplicationParams{
Context: ctx,
Execution: &models.StartReplicationExecution{
PolicyID: policyID,
},
})
if err != nil {
return nil, err
}
return response, nil
}
func StopReplication(policyID int64) (*replication.StopReplicationOK, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
}
response, err := client.Replication.StopReplication(ctx, &replication.StopReplicationParams{
ID: policyID,
})
if err != nil {
return nil, err
}
return response, nil
}
func GetReplicationExecutions(policyID int64) (*replication.ListReplicationExecutionsOK, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
}
response, err := client.Replication.ListReplicationExecutions(ctx, &replication.ListReplicationExecutionsParams{
PolicyID: &policyID,
})
if err != nil {
return nil, err
}
return response, nil
}
func GetReplicationExecution(executionID int64) (*replication.GetReplicationExecutionOK, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
}
response, err := client.Replication.GetReplicationExecution(ctx, &replication.GetReplicationExecutionParams{
ID: executionID,
})
if err != nil {
return nil, err
}
return response, nil
}

View File

@ -108,7 +108,7 @@ func DeleteRobot(robotID int64) error {
return nil
}
func CreateRobot(opts create.CreateView, kind string) (*robot.CreateRobotCreated, error) {
func CreateRobot(opts create.CreateView) (*robot.CreateRobotCreated, error) {
ctx, client, err := utils.ContextWithClient()
if err != nil {
return nil, err
@ -122,11 +122,12 @@ func CreateRobot(opts create.CreateView, kind string) (*robot.CreateRobotCreated
for _, perm := range permissions {
convertedPerm := &models.RobotPermission{
Access: perm.Access,
Kind: kind,
Namespace: opts.ProjectName,
Kind: perm.Kind,
Namespace: perm.Namespace,
}
convertedPerms = append(convertedPerms, convertedPerm)
}
response, err := client.Robot.CreateRobot(
ctx,
&robot.CreateRobotParams{
@ -134,14 +135,14 @@ func CreateRobot(opts create.CreateView, kind string) (*robot.CreateRobotCreated
Description: opts.Description,
Disable: false,
Duration: opts.Duration,
Level: kind,
Level: opts.Level,
Name: opts.Name,
Permissions: convertedPerms,
},
},
)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create robot: %v", utils.ParseHarborErrorMsg(err))
}
log.Info("robot created successfully.")
@ -160,21 +161,21 @@ func UpdateRobot(opts *update.UpdateView) error {
permissions := opts.Permissions
convertedPerms := make([]*models.RobotPermission, 0, len(permissions))
// Loop through original permissions and convert them
for _, perm := range permissions {
convertedPerm := &models.RobotPermission{
Access: perm.Access,
Kind: opts.Permissions[0].Kind,
Namespace: opts.Permissions[0].Namespace,
Kind: perm.Kind,
Namespace: perm.Namespace,
}
convertedPerms = append(convertedPerms, convertedPerm)
}
_, err = client.Robot.UpdateRobot(
ctx,
&robot.UpdateRobotParams{
Robot: &models.Robot{
Description: opts.Description,
Duration: opts.Duration,
Duration: &opts.Duration,
Editable: opts.Editable,
Disable: opts.Disable,
ID: opts.ID,

View File

@ -0,0 +1,257 @@
// 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 config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/goharbor/harbor-cli/pkg/views/replication/policies/create"
"gopkg.in/yaml.v2"
)
type PolicyConfig struct {
Name string `yaml:"name" json:"name"`
Description string `yaml:"description" json:"description"`
ReplicationMode string `yaml:"replication_mode,omitempty" json:"replication_mode,omitempty"`
Filter []*ReplicationFilter `yaml:"replication_filter,omitempty" json:"replication_filter,omitempty"`
TargetRegistry string `yaml:"target_registry,omitempty" json:"target_registry,omitempty"`
TriggerMode string `yaml:"trigger_mode,omitempty" json:"trigger_mode,omitempty"`
BandWidthLimit string `yaml:"bandwidth_limit,omitempty" json:"bandwidth_limit,omitempty"`
CronString string `yaml:"cron_string,omitempty" json:"cron_string,omitempty"`
Override bool `yaml:"override,omitempty" json:"override,omitempty"`
ReplicateDeletion bool `yaml:"replicate_deletion,omitempty" json:"replicate_deletion,omitempty"`
CopyByChunk bool `yaml:"copy_by_chunk,omitempty" json:"copy_by_chunk,omitempty"`
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
}
type ReplicationFilter struct {
Type string `yaml:"type,omitempty" json:"type,omitempty"`
Decoration string `yaml:"decoration,omitempty" json:"decoration,omitempty"`
Value string `yaml:"value,omitempty" json:"value,omitempty"`
}
func LoadConfigFromFile(filename string) (*create.CreateView, error) {
var opts *create.CreateView
var err error
ext := filepath.Ext(filename)
if ext == "" {
return nil, fmt.Errorf("file must have an extension (.yaml, .yml, or .json)")
}
fileType := ext[1:]
if fileType == "yml" {
fileType = "yaml"
}
opts, err = LoadConfigFromYAMLorJSON(filename, fileType)
if err != nil {
return nil, fmt.Errorf("failed to load configuration: %v", err)
}
return opts, nil
}
func LoadConfigFromYAMLorJSON(filename string, fileType string) (*create.CreateView, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read file: %v", err)
}
var config PolicyConfig
switch fileType {
case "yaml", "yml":
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse YAML: %v", err)
}
case "json":
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %v", err)
}
default:
return nil, fmt.Errorf("unsupported file type: %s, expected 'yaml' or 'json'", fileType)
}
if err := validateConfig(&config); err != nil {
return nil, fmt.Errorf("configuration validation failed: %v", err)
}
opts := &create.CreateView{
Name: config.Name,
Description: config.Description,
ReplicationMode: normalizeReplicationMode(config.ReplicationMode),
TriggerType: normalizeTriggerMode(config.TriggerMode),
TargetRegistry: config.TargetRegistry,
CronString: config.CronString,
Override: config.Override,
CopyByChunk: config.CopyByChunk,
ReplicateDeletion: config.ReplicateDeletion,
Speed: config.BandWidthLimit,
Enabled: config.Enabled,
}
if err := processFilters(&config, opts); err != nil {
return nil, fmt.Errorf("failed to process filters: %v", err)
}
return opts, nil
}
func validateConfig(config *PolicyConfig) error {
if config.Name == "" {
return fmt.Errorf("name is required")
}
if config.ReplicationMode != "" {
mode := strings.ToLower(config.ReplicationMode)
if mode != "push" && mode != "pull" {
return fmt.Errorf("replication_mode must be 'push' or 'pull', got: %s", config.ReplicationMode)
}
}
if config.TriggerMode != "" {
mode := strings.ToLower(config.TriggerMode)
validTriggers := []string{"manual", "scheduled", "event_based"}
isValid := false
for _, valid := range validTriggers {
if mode == valid {
isValid = true
break
}
}
if !isValid {
return fmt.Errorf("trigger_mode must be one of [manual, scheduled, event_based], got: %s", config.TriggerMode)
}
}
for i, filter := range config.Filter {
if err := validateFilter(filter, i); err != nil {
return err
}
}
return nil
}
func validateFilter(filter *ReplicationFilter, index int) error {
if filter.Type == "" {
return fmt.Errorf("filter[%d]: type is required", index)
}
validTypes := []string{"resource", "name", "tag", "label"}
isValidType := false
for _, validType := range validTypes {
if filter.Type == validType {
isValidType = true
break
}
}
if !isValidType {
return fmt.Errorf("filter[%d]: type must be one of [resource, name, tag, label], got: %s", index, filter.Type)
}
if filter.Type == "resource" {
if filter.Value != "" {
validResources := []string{"image", "artifact"}
isValidResource := false
for _, validResource := range validResources {
if filter.Value == validResource {
isValidResource = true
break
}
}
if !isValidResource {
return fmt.Errorf("filter[%d]: resource value must be 'image' or 'chart', got: %s", index, filter.Value)
}
}
}
if filter.Decoration != "" {
if filter.Type != "tag" && filter.Type != "label" {
return fmt.Errorf("filter[%d]: decoration is only supported for 'tag' and 'label' filters, got type: %s", index, filter.Type)
}
validDecorations := []string{"matches", "excludes"}
isValidDecoration := false
for _, validDecoration := range validDecorations {
if filter.Decoration == validDecoration {
isValidDecoration = true
break
}
}
if !isValidDecoration {
return fmt.Errorf("filter[%d]: decoration must be 'matches' or 'excludes', got: %s", index, filter.Decoration)
}
}
return nil
}
func normalizeReplicationMode(mode string) string {
switch strings.ToLower(mode) {
case "push":
return "Push"
case "pull":
return "Pull"
default:
return mode
}
}
func normalizeTriggerMode(mode string) string {
switch strings.ToLower(mode) {
case "manual":
return "manual"
case "scheduled":
return "scheduled"
case "event_based":
return "event_based"
default:
return mode
}
}
func processFilters(config *PolicyConfig, opts *create.CreateView) error {
for _, filter := range config.Filter {
switch filter.Type {
case "resource":
if filter.Value == "" {
opts.ResourceFilter = "All"
} else {
opts.ResourceFilter = filter.Value
}
case "name":
opts.NameFilter = filter.Value
case "tag":
if filter.Decoration != "" {
opts.TagFilter = filter.Decoration
} else {
opts.TagFilter = "matches"
}
opts.TagPattern = filter.Value
case "label":
if filter.Decoration != "" {
opts.LabelFilter = filter.Decoration
} else {
opts.LabelFilter = "matches"
}
opts.LabelPattern = filter.Value
}
}
return nil
}

View File

@ -1,243 +0,0 @@
// 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 config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/views/robot/create"
"gopkg.in/yaml.v2"
)
type RobotPermissionConfig struct {
Name string `yaml:"name" json:"name"`
Description string `yaml:"description" json:"description"`
Duration int64 `yaml:"duration" json:"duration"`
Project string `yaml:"project" json:"project"`
Permissions []PermissionSpec `yaml:"permissions" json:"permissions"`
}
type PermissionSpec struct {
Resource string `yaml:"resource,omitempty" json:"resource,omitempty"`
Resources []string `yaml:"resources,omitempty" json:"resources,omitempty"`
Actions []string `yaml:"actions" json:"actions"`
}
type RobotSecret struct {
Name string `json:"name"`
ExpiresAt int64 `json:"expires_at"`
CreationTime string `json:"creation_time"`
Secret string `json:"secret"`
}
func LoadRobotConfigFromYAMLorJSON(filename string, fileType string) (*create.CreateView, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read YAML file: %v", err)
}
var config RobotPermissionConfig
if fileType == "yaml" {
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse YAML: %v", err)
}
} else if fileType == "json" {
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %v", err)
}
} else {
return nil, fmt.Errorf("unsupported file type: %s, expected 'yaml' or 'json'", fileType)
}
opts := &create.CreateView{
Name: config.Name,
Description: config.Description,
Duration: config.Duration,
ProjectName: config.Project,
}
permissions, err := ProcessPermissions(config.Permissions)
if err != nil {
return nil, err
}
var accesses []*models.Access
for _, perm := range permissions {
access := &models.Access{
Action: perm.Action,
Resource: perm.Resource,
}
accesses = append(accesses, access)
}
perm := &create.RobotPermission{
Namespace: config.Project,
Access: accesses,
}
opts.Permissions = []*create.RobotPermission{perm}
return opts, nil
}
func ProcessPermissions(specs []PermissionSpec) ([]models.Permission, error) {
var result []models.Permission
availablePerms, err := GetAllAvailablePermissions()
if err != nil {
return nil, err
}
for _, spec := range specs {
var resources []string
if spec.Resource != "" {
resources = []string{spec.Resource}
} else if len(spec.Resources) > 0 {
resources = spec.Resources
} else {
return nil, fmt.Errorf("permission must specify either 'resource' or 'resources'")
}
if containsWildcard(resources) {
resources = getAllResourceNames(availablePerms)
}
for _, resource := range resources {
if !isValidResource(resource, availablePerms) && resource != "*" {
fmt.Printf("Warning: Resource '%s' is not valid and will be skipped\n", resource)
continue
}
if containsWildcard(spec.Actions) {
validActions := getValidActionsForResource(resource, availablePerms)
for _, action := range validActions {
result = append(result, models.Permission{
Resource: resource,
Action: action,
})
}
} else {
for _, action := range spec.Actions {
if isValidAction(resource, action, availablePerms) {
result = append(result, models.Permission{
Resource: resource,
Action: action,
})
} else {
fmt.Printf("Warning: Action '%s' is not valid for resource '%s' and will be skipped\n",
action, resource)
}
}
}
}
}
return result, nil
}
func LoadRobotConfigFromFile(filename string) (*create.CreateView, error) {
var opts *create.CreateView
var err error
opts, err = LoadRobotConfigFromYAMLorJSON(filename, filepath.Ext(filename)[1:])
if err != nil {
return nil, fmt.Errorf("failed to load configuration: %v", err)
}
if opts.Name == "" {
return nil, fmt.Errorf("robot name cannot be empty")
}
if opts.Duration == 0 {
return nil, fmt.Errorf("duration cannot be 0")
}
if opts.ProjectName == "" {
return nil, fmt.Errorf("project name cannot be empty")
}
if len(opts.Permissions) == 0 || len(opts.Permissions[0].Access) == 0 {
return nil, fmt.Errorf("no permissions specified")
}
projectExists := false
projectsResp, err := api.ListAllProjects()
if err != nil {
return nil, fmt.Errorf("failed to list projects: %v", err)
}
for _, proj := range projectsResp.Payload {
if proj.Name == opts.ProjectName {
projectExists = true
break
}
}
if !projectExists {
return nil, fmt.Errorf("project '%s' does not exist in Harbor", opts.ProjectName)
}
return opts, nil
}
func GetAllAvailablePermissions() (map[string][]string, error) {
permsResp, err := api.GetPermissions()
if err != nil {
return nil, fmt.Errorf("failed to get permissions: %v", err)
}
result := make(map[string][]string)
for _, perm := range permsResp.Payload.Project {
resource := perm.Resource
if _, exists := result[resource]; !exists {
result[resource] = []string{}
}
result[resource] = append(result[resource], perm.Action)
}
return result, nil
}
func containsWildcard(items []string) bool {
return slices.Contains(items, "*")
}
func getAllResourceNames(permissions map[string][]string) []string {
resources := make([]string, 0, len(permissions))
for resource := range permissions {
resources = append(resources, resource)
}
return resources
}
func isValidResource(resource string, permissions map[string][]string) bool {
_, exists := permissions[resource]
return exists
}
func isValidAction(resource, action string, permissions map[string][]string) bool {
actions, exists := permissions[resource]
if !exists {
return false
}
return slices.Contains(actions, action)
}
func getValidActionsForResource(resource string, permissions map[string][]string) []string {
if actions, exists := permissions[resource]; exists {
return actions
}
return []string{}
}

402
pkg/config/robot/robot.go Normal file
View File

@ -0,0 +1,402 @@
// 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.
// 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 config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/api"
"github.com/goharbor/harbor-cli/pkg/views/robot/create"
"gopkg.in/yaml.v2"
)
// PermissionMap holds permissions separated by scope
type PermissionMap struct {
Project map[string][]string
System map[string][]string
}
// RobotPermissionConfig represents the robot account configuration from file
type RobotPermissionConfig struct {
Name string `yaml:"name" json:"name"`
Description string `yaml:"description" json:"description"`
Duration int64 `yaml:"duration" json:"duration"`
Level string `yaml:"level,omitempty" json:"level,omitempty"` // "project" or "system"
Permissions []PermissionScope `yaml:"permissions" json:"permissions"`
}
// PermissionScope represents a permission scope with access items, kind and namespace
type PermissionScope struct {
Access []AccessItem `yaml:"access" json:"access"`
Kind string `yaml:"kind" json:"kind"` // "project" or "system"
Namespace string `yaml:"namespace" json:"namespace"` // Project name or "/"
}
// AccessItem represents a resource permission definition
type AccessItem struct {
Resource string `yaml:"resource,omitempty" json:"resource,omitempty"`
Resources []string `yaml:"resources,omitempty" json:"resources,omitempty"`
Actions []string `yaml:"actions" json:"actions"`
}
type RobotSecret struct {
Name string `json:"name"`
ExpiresAt int64 `json:"expires_at"`
CreationTime string `json:"creation_time"`
Secret string `json:"secret"`
}
func LoadRobotConfigFromYAMLorJSON(filename string, fileType string) (*create.CreateView, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read file: %v", err)
}
var config RobotPermissionConfig
if fileType == "yaml" {
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse YAML: %v", err)
}
} else if fileType == "json" {
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %v", err)
}
} else {
return nil, fmt.Errorf("unsupported file type: %s, expected 'yaml' or 'json'", fileType)
}
// Determine the robot level and ensure it's lowercase
robotLevel := "project" // Default to project robot
if config.Level != "" {
robotLevel = strings.ToLower(config.Level)
}
// Create the base view object
opts := &create.CreateView{
Name: config.Name,
Description: config.Description,
Duration: config.Duration,
Level: robotLevel, // Keep lowercase
}
// Process permissions
robotPermissions, err := processPermissionScopes(config.Permissions, opts.Level)
if err != nil {
return nil, err
}
if len(robotPermissions) == 0 {
return nil, fmt.Errorf("no permissions defined in the configuration")
}
opts.Permissions = robotPermissions
// If this is a project robot, set the ProjectName from the first permission's namespace
if opts.Level == "project" && len(opts.Permissions) > 0 {
opts.ProjectName = opts.Permissions[0].Namespace
}
return opts, nil
}
// Process permission scopes
func processPermissionScopes(scopes []PermissionScope, robotLevel string) ([]*create.RobotPermission, error) {
var result []*create.RobotPermission
// Get available permissions for validation
availablePerms, err := GetAllAvailablePermissions()
if err != nil {
return nil, err
}
// Check if we're creating a project robot
isProjectRobot := robotLevel == "project"
// Process each permission scope
for _, scope := range scopes {
// Make sure kind is lowercase
scopeKind := strings.ToLower(scope.Kind)
// For project robots, enforce only project permissions
if isProjectRobot && scopeKind != "project" {
return nil, fmt.Errorf("project robots can only have project permission scopes, found %s", scopeKind)
}
robotPerm := &create.RobotPermission{
Kind: scopeKind,
Namespace: scope.Namespace,
Access: []*models.Access{},
}
// Process access items in this scope
for _, accessItem := range scope.Access {
// Get the list of resources
var resources []string
if accessItem.Resource != "" {
resources = []string{accessItem.Resource}
} else if len(accessItem.Resources) > 0 {
resources = accessItem.Resources
} else {
return nil, fmt.Errorf("permission must specify either 'resource' or 'resources'")
}
// Handle wildcard for resources
if containsWildcard(resources) {
// Get appropriate resources based on scope kind
if scopeKind == "project" {
resources = getAllResourceNames(availablePerms.Project)
} else {
resources = getAllResourceNames(availablePerms.System)
}
}
// Process each resource
for _, resource := range resources {
// Determine which permission map to use based on scope kind
var validActions []string
var isValid bool
if scopeKind == "project" {
isValid = isValidResource(resource, availablePerms.Project)
validActions = getValidActionsForResource(resource, availablePerms.Project)
} else {
isValid = isValidResource(resource, availablePerms.System)
validActions = getValidActionsForResource(resource, availablePerms.System)
}
if !isValid && resource != "*" {
fmt.Printf("Warning: Resource '%s' is not valid for scope '%s' and will be skipped\n",
resource, scopeKind)
continue
}
// Handle wildcard for actions
if containsWildcard(accessItem.Actions) {
for _, action := range validActions {
robotPerm.Access = append(robotPerm.Access, &models.Access{
Resource: resource,
Action: action,
})
}
} else {
// Process specific actions
for _, action := range accessItem.Actions {
var actionValid bool
if scopeKind == "project" {
actionValid = isValidAction(resource, action, availablePerms.Project)
} else {
actionValid = isValidAction(resource, action, availablePerms.System)
}
if actionValid {
robotPerm.Access = append(robotPerm.Access, &models.Access{
Resource: resource,
Action: action,
})
} else {
fmt.Printf("Warning: Action '%s' is not valid for resource '%s' in scope '%s' and will be skipped\n",
action, resource, scopeKind)
}
}
}
}
}
// Add the permission scope if it has any access items
if len(robotPerm.Access) > 0 {
result = append(result, robotPerm)
}
}
// For project robots, ensure there's only one permission scope
if isProjectRobot && len(result) > 1 {
return nil, fmt.Errorf("project robots can only have one permission scope, found %d", len(result))
}
return result, nil
}
func LoadRobotConfigFromFile(filename string) (*create.CreateView, error) {
var opts *create.CreateView
var err error
ext := filepath.Ext(filename)
if ext == "" {
return nil, fmt.Errorf("file must have an extension (.yaml, .yml, or .json)")
}
fileType := ext[1:] // Remove the dot
if fileType == "yml" {
fileType = "yaml"
}
opts, err = LoadRobotConfigFromYAMLorJSON(filename, fileType)
if err != nil {
return nil, fmt.Errorf("failed to load configuration: %v", err)
}
// Basic validation
if opts.Name == "" {
return nil, fmt.Errorf("robot name cannot be empty")
}
if opts.Duration == 0 {
return nil, fmt.Errorf("duration cannot be 0")
}
// Level-specific validation
if opts.Level == "project" {
// Project robot requires a project
projectName := ""
if opts.ProjectName != "" {
projectName = opts.ProjectName
} else if len(opts.Permissions) > 0 && opts.Permissions[0].Namespace != "" {
projectName = opts.Permissions[0].Namespace
}
if projectName == "" {
return nil, fmt.Errorf("project name is required for project robots")
}
// Verify project exists
projectExists := false
projectsResp, err := api.ListAllProjects()
if err != nil {
return nil, fmt.Errorf("failed to list projects: %v", err)
}
for _, proj := range projectsResp.Payload {
if proj.Name == projectName {
projectExists = true
break
}
}
if !projectExists {
return nil, fmt.Errorf("project '%s' does not exist in Harbor", projectName)
}
// Set the project name consistently
opts.ProjectName = projectName
} else if opts.Level == "system" {
// System robot validation
if len(opts.Permissions) == 0 {
return nil, fmt.Errorf("system robot must have at least one permission scope")
}
} else {
return nil, fmt.Errorf("invalid robot level: %s. Must be 'project' or 'system'", opts.Level)
}
// Validate permissions
if len(opts.Permissions) == 0 {
return nil, fmt.Errorf("no permissions specified")
}
for _, perm := range opts.Permissions {
if len(perm.Access) == 0 {
return nil, fmt.Errorf("no access defined for permission scope")
}
}
return opts, nil
}
// GetAllAvailablePermissions returns permissions organized by scope
func GetAllAvailablePermissions() (*PermissionMap, error) {
permsResp, err := api.GetPermissions()
if err != nil {
return nil, fmt.Errorf("failed to get permissions: %v", err)
}
result := &PermissionMap{
Project: make(map[string][]string),
System: make(map[string][]string),
}
// Add project permissions
for _, perm := range permsResp.Payload.Project {
resource := perm.Resource
if _, exists := result.Project[resource]; !exists {
result.Project[resource] = []string{}
}
result.Project[resource] = append(result.Project[resource], perm.Action)
}
// Add system permissions if available
if permsResp.Payload.System != nil {
for _, perm := range permsResp.Payload.System {
resource := perm.Resource
if _, exists := result.System[resource]; !exists {
result.System[resource] = []string{}
}
result.System[resource] = append(result.System[resource], perm.Action)
}
}
return result, nil
}
func containsWildcard(items []string) bool {
return slices.Contains(items, "*")
}
func getAllResourceNames(permissions map[string][]string) []string {
resources := make([]string, 0, len(permissions))
for resource := range permissions {
resources = append(resources, resource)
}
return resources
}
func isValidResource(resource string, permissions map[string][]string) bool {
_, exists := permissions[resource]
return exists
}
func isValidAction(resource, action string, permissions map[string][]string) bool {
actions, exists := permissions[resource]
if !exists {
return false
}
return slices.Contains(actions, action)
}
func getValidActionsForResource(resource string, permissions map[string][]string) []string {
if actions, exists := permissions[resource]; exists {
return actions
}
return []string{}
}

View File

@ -33,6 +33,9 @@ import (
pview "github.com/goharbor/harbor-cli/pkg/views/project/select"
qview "github.com/goharbor/harbor-cli/pkg/views/quota/select"
rview "github.com/goharbor/harbor-cli/pkg/views/registry/select"
rexecutions "github.com/goharbor/harbor-cli/pkg/views/replication/execution/select"
rpolicies "github.com/goharbor/harbor-cli/pkg/views/replication/policies/select"
repoView "github.com/goharbor/harbor-cli/pkg/views/repository/select"
robotView "github.com/goharbor/harbor-cli/pkg/views/robot/select"
sview "github.com/goharbor/harbor-cli/pkg/views/scanner/select"
@ -258,11 +261,11 @@ func GetActiveContextFromUser() (string, error) {
return res, nil
}
func GetRobotPermissionsFromUser() []models.Permission {
func GetRobotPermissionsFromUser(kind string) []models.Permission {
permissions := make(chan []models.Permission)
go func() {
response, _ := api.GetPermissions()
robotView.ListPermissions(response.Payload, permissions)
robotView.ListPermissions(response.Payload, kind, permissions)
}()
return <-permissions
}
@ -270,7 +273,9 @@ func GetRobotPermissionsFromUser() []models.Permission {
func GetRobotIDFromUser(projectID int64) int64 {
robotID := make(chan int64)
var opts api.ListFlags
opts.Q = constants.ProjectQString + strconv.FormatInt(projectID, 10)
if projectID != -1 {
opts.Q = constants.ProjectQString + strconv.FormatInt(projectID, 10)
}
go func() {
response, _ := api.ListRobot(opts)
@ -278,3 +283,34 @@ func GetRobotIDFromUser(projectID int64) int64 {
}()
return <-robotID
}
func GetReplicationPolicyFromUser() int64 {
replicationPolicyID := make(chan int64)
go func() {
response, err := api.ListReplicationPolicies()
if err != nil {
log.Fatal(err)
}
rpolicies.ReplicationPoliciesList(response.Payload, replicationPolicyID)
}()
return <-replicationPolicyID
}
func GetReplicationExecutionIDFromUser(rpolicyID int64) int64 {
executionID := make(chan int64)
go func() {
response, err := api.GetReplicationExecutions(rpolicyID)
if err != nil {
log.Fatal(err)
}
if len(response.Payload) == 0 {
log.Fatal("no replication executions found")
}
rexecutions.ReplicationExecutionList(response.Payload, executionID)
}()
return <-executionID
}

View File

@ -180,3 +180,11 @@ func GetSecretStdin(prompt string) (string, error) {
func ToKebabCase(s string) string {
return strings.ReplaceAll(strings.ToLower(s), " ", "-")
}
func Capitalize(s string) string {
if s == "" {
return ""
}
return s
// trings.ToUpper(s[:1]) + s[1:]
}

View File

@ -14,6 +14,7 @@
package view
import (
"encoding/json"
"fmt"
"os"
"strconv"
@ -26,48 +27,239 @@ import (
"github.com/goharbor/harbor-cli/pkg/views/base/tablelist"
)
var columns = []table.Column{
{Title: "ID", Width: tablelist.WidthS},
{Title: "Tags", Width: tablelist.WidthL},
{Title: "Artifact Digest", Width: tablelist.WidthXL},
{Title: "Type", Width: tablelist.WidthS},
{Title: "Size", Width: tablelist.WidthM},
{Title: "Vulnerabilities", Width: tablelist.WidthL},
{Title: "Push Time", Width: tablelist.WidthL},
var detailsColumns = []table.Column{
{Title: "Attribute", Width: tablelist.WidthL * 2},
{Title: "Value", Width: tablelist.Width3XL * 2},
}
func ViewArtifact(artifact *models.Artifact) {
var rows []table.Row
displayDetailTable(artifact)
}
func displayDetailTable(artifact *models.Artifact) {
var basicRows []table.Row
var otherRows []table.Row
// BASIC INFO
addRow(&basicRows, "ID", strconv.FormatInt(int64(artifact.ID), 10))
addRow(&basicRows, "Repository ID", strconv.FormatInt(int64(artifact.RepositoryID), 10))
addRow(&basicRows, "Digest", artifact.Digest)
addRow(&basicRows, "Media Type", artifact.MediaType)
addRow(&basicRows, "Type", artifact.Type)
// Tags
if len(artifact.Tags) > 0 {
var tagNames []string
for _, tag := range artifact.Tags {
tagNames = append(tagNames, tag.Name)
}
addMultiLineData(&basicRows, "Tags", tagNames, ", ", 80)
}
addRow(&basicRows, "Size", utils.FormatSize(artifact.Size))
pushTime, _ := utils.FormatCreatedTime(artifact.PushTime.String())
artifactSize := utils.FormatSize(artifact.Size)
var tagNames []string
for _, tag := range artifact.Tags {
tagNames = append(tagNames, tag.Name)
}
tags := "-"
if len(tagNames) > 0 {
tags = strings.Join(tagNames, ", ")
addRow(&basicRows, "Push Time", pushTime)
// Platform Info from ExtraAttrs
if artifact.ExtraAttrs != nil {
if arch, ok := artifact.ExtraAttrs["architecture"].(string); ok {
addRow(&basicRows, "Architecture", arch)
}
if osVal, ok := artifact.ExtraAttrs["os"].(string); ok {
addRow(&basicRows, "OS", osVal)
}
if created, ok := artifact.ExtraAttrs["created"].(string); ok {
addRow(&basicRows, "Created", created)
}
}
var totalVulnerabilities int64
for _, scan := range artifact.ScanOverview {
totalVulnerabilities += scan.Summary.Total
// Author from config
if artifact.ExtraAttrs != nil {
if config, ok := artifact.ExtraAttrs["config"].(map[string]any); ok {
if author, ok := config["author"].(string); ok {
addRow(&basicRows, "Author", author)
}
}
}
rows = append(rows, table.Row{
strconv.FormatInt(int64(artifact.ID), 10),
tags,
artifact.Digest[:16],
artifact.Type,
artifactSize,
strconv.FormatInt(totalVulnerabilities, 10),
pushTime,
})
m := tablelist.NewModel(columns, rows, len(rows))
// Version (if present in ExtraAttrs)
if artifact.ExtraAttrs != nil {
if version, ok := artifact.ExtraAttrs["version"].(string); ok {
addRow(&basicRows, "Version", version)
}
if apiVersion, ok := artifact.ExtraAttrs["apiVersion"].(string); ok {
addRow(&basicRows, "API Version", apiVersion)
}
if appVersion, ok := artifact.ExtraAttrs["appVersion"].(string); ok {
addRow(&basicRows, "App Version", appVersion)
}
if description, ok := artifact.ExtraAttrs["description"].(string); ok {
addRow(&basicRows, "Description", description)
}
}
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Println("Error running program:", err)
// OTHER INFO
// Config details
if artifact.ExtraAttrs != nil {
if config, ok := artifact.ExtraAttrs["config"].(map[string]any); ok {
// Environment Variables
if env, ok := config["Env"].([]any); ok && len(env) > 0 {
var envStrings []string
for _, e := range env {
envStrings = append(envStrings, fmt.Sprintf("%v", e))
}
addMultiLineData(&otherRows, "Environment Variables", envStrings, "", 0)
}
// Exposed Ports
if ports, ok := config["ExposedPorts"].(map[string]any); ok && len(ports) > 0 {
var portsList []string
for port := range ports {
portsList = append(portsList, port)
}
addMultiLineData(&otherRows, "Exposed Ports", portsList, "", 0)
}
// Volumes
if volumes, ok := config["Volumes"].(map[string]any); ok && len(volumes) > 0 {
var volumesList []string
for volume := range volumes {
volumesList = append(volumesList, volume)
}
addMultiLineData(&otherRows, "Volumes", volumesList, "", 0)
}
// Entrypoint
if entrypoint, ok := config["Entrypoint"].([]any); ok && len(entrypoint) > 0 {
var entryList []string
for _, e := range entrypoint {
entryList = append(entryList, fmt.Sprintf("%v", e))
}
addRow(&otherRows, "Entrypoint", strings.Join(entryList, " "))
}
// Command
if cmd, ok := config["Cmd"].([]any); ok && len(cmd) > 0 {
var cmdList []string
for _, c := range cmd {
cmdList = append(cmdList, fmt.Sprintf("%v", c))
}
addRow(&otherRows, "Command", strings.Join(cmdList, " "))
}
// Labels
if labels, ok := config["Labels"].(map[string]any); ok && len(labels) > 0 {
var labelsList []string
for k, v := range labels {
labelsList = append(labelsList, fmt.Sprintf("%s: %v", k, v))
}
addMultiLineData(&otherRows, "Labels", labelsList, "", 0)
}
}
}
// Other ExtraAttrs (excluding those already shown)
if artifact.ExtraAttrs != nil {
for key, value := range artifact.ExtraAttrs {
if key == "architecture" || key == "os" || key == "config" || key == "created" || key == "layers" || key == "version" || key == "description" || key == "apiVersion" || key == "appVersion" {
continue
}
switch v := value.(type) {
case string, bool, int, int64, float64:
addRow(&otherRows, key, fmt.Sprintf("%v", v))
default:
jsonBytes, err := json.MarshalIndent(value, "", " ")
if err == nil {
lines := strings.Split(string(jsonBytes), "\n")
addMultiLineData(&otherRows, key, lines, "", 0)
} else {
addRow(&otherRows, key, "(complex structure - see JSON output)")
}
}
}
}
if len(artifact.References) > 0 {
for i, ref := range artifact.References {
if ref.Platform != nil && ref.Platform.Architecture != "" {
addRow(&otherRows, fmt.Sprintf("Reference %d", i+1), fmt.Sprintf("%s: %s", ref.Platform.Architecture, ref.ChildDigest))
}
}
}
// Additional links
if len(artifact.AdditionLinks) > 0 {
var links []string
for key := range artifact.AdditionLinks {
links = append(links, key)
}
addRow(&otherRows, "Available Endpoints", strings.Join(links, ", "))
}
// Display the tables
fmt.Println("\nBASIC INFORMATION")
basicTable := tablelist.NewModel(detailsColumns, basicRows, len(basicRows))
if _, err := tea.NewProgram(basicTable).Run(); err != nil {
fmt.Println("Error displaying basic artifact details:", err)
os.Exit(1)
}
fmt.Println("\nOTHER INFORMATION")
otherTable := tablelist.NewModel(detailsColumns, otherRows, len(otherRows))
if _, err := tea.NewProgram(otherTable).Run(); err != nil {
fmt.Println("Error displaying other artifact details:", err)
os.Exit(1)
}
}
func addRow(rows *[]table.Row, key string, value string) {
*rows = append(*rows, table.Row{key, value})
}
func addMultiLineData(rows *[]table.Row, key string, values []string, separator string, maxWidth int) {
if len(values) == 0 {
return
}
firstValue := values[0]
if separator != "" && maxWidth > 0 {
if len(firstValue) > maxWidth {
addRow(rows, key, firstValue)
} else {
currentLine := firstValue
nextIndex := 1
for nextIndex < len(values) {
nextValue := values[nextIndex]
if len(currentLine)+len(separator)+len(nextValue) <= maxWidth {
currentLine += separator + nextValue
nextIndex++
} else {
break
}
}
addRow(rows, key, currentLine)
currentLine = ""
for i := nextIndex; i < len(values); i++ {
nextValue := values[i]
if currentLine == "" {
currentLine = nextValue
} else if len(currentLine)+len(separator)+len(nextValue) <= maxWidth {
currentLine += separator + nextValue
} else {
addRow(rows, "", currentLine)
currentLine = nextValue
}
}
if currentLine != "" {
addRow(rows, "", currentLine)
}
return
}
} else {
addRow(rows, key, firstValue)
}
for i := 1; i < len(values); i++ {
addRow(rows, "", values[i])
}
}

54
pkg/views/logs/view.go Normal file
View File

@ -0,0 +1,54 @@
// 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 (
"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: "User", Width: 18},
{Title: "Resource", Width: 18},
{Title: "Resource Type", Width: 14},
{Title: "Operation", Width: 10},
{Title: "Time", Width: 16},
}
func ListLogs(logs []*models.AuditLogExt) {
var rows []table.Row
for _, log := range logs {
operationTime, _ := utils.FormatCreatedTime(log.OpTime.String())
rows = append(rows, table.Row{
log.Username,
log.Resource,
log.ResourceType,
log.Operation,
operationTime,
})
}
m := tablelist.NewModel(columns, rows, len(rows))
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}

View File

@ -0,0 +1,52 @@
// 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 execution
import (
"fmt"
"os"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/views/base/selection"
)
func ReplicationExecutionList(executions []*models.ReplicationExecution, choice chan<- int64) {
itemsList := make([]list.Item, len(executions))
for i, p := range executions {
displayName := fmt.Sprintf("ID: %d, Status: %s, Trigger: %s, Start Time: %s, Succeed: %d, Total: %d",
p.ID, p.Status, p.Trigger, p.StartTime.String(), p.Succeed, p.Total)
itemsList[i] = selection.Item(displayName)
}
m := selection.NewModel(itemsList, "Select a Replication Execution")
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 {
// Extract the ID from p.Choice
var execID int64
_, err = fmt.Sscanf(p.Choice, "ID: %d", &execID)
if err != nil {
fmt.Println("error parsing execution ID:", err)
os.Exit(1)
}
choice <- execID
}
}

View File

@ -0,0 +1,60 @@
// 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 view
import (
"fmt"
"os"
"strconv"
"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.WidthL},
{Title: "Status", Width: tablelist.Width3XL},
{Title: "Trigger", Width: tablelist.WidthL},
{Title: "Start Time", Width: tablelist.WidthL},
{Title: "Succed", Width: tablelist.WidthL},
{Title: "Total", Width: tablelist.WidthL},
}
func ViewExecutions(executions []*models.ReplicationExecution) {
var rows []table.Row
for _, exec := range executions {
startTime, err := utils.FormatCreatedTime(exec.StartTime.String())
if err != nil {
fmt.Println("Error formatting start time:", err)
startTime = exec.StartTime.String()
}
rows = append(rows, table.Row{
strconv.FormatInt(exec.ID, 10),
exec.Status,
exec.Trigger,
startTime,
strconv.FormatInt(exec.Succeed, 10),
strconv.FormatInt(exec.Total, 10),
})
}
m := tablelist.NewModel(columns, rows, len(rows))
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}

View File

@ -0,0 +1,297 @@
// 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 create
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
log "github.com/sirupsen/logrus"
)
type CreateView struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Enabled bool `json:"enabled,omitempty"`
ReplicationMode string `json:"mode,omitempty"`
Override bool `json:"override,omitempty"`
CopyByChunk bool `json:"copy_by_chunk,omitempty"`
Speed string `json:"speed,omitempty"`
TargetRegistry string `json:"target_registry,omitempty"` // ID of the target registry
// Trigger related fields
TriggerType string `json:"trigger_type,omitempty"`
ReplicateDeletion bool `json:"replicate_deletion,omitempty"`
CronString string `json:"cron_string,omitempty"`
// Filter related fields
ResourceFilter string `json:"resource_filter,omitempty"` // All, image, or chart
NameFilter string `json:"name_filter,omitempty"`
TagFilter string `json:"tag_filter,omitempty"` // matching or excluding
TagPattern string `json:"tag_pattern,omitempty"`
LabelFilter string `json:"label_filter,omitempty"` // matching or excluding
LabelPattern string `json:"label_pattern,omitempty"` // label key=value pairs
}
func CreateRPolicyView(createView *CreateView, update bool) {
if createView.TriggerType == "" {
createView.TriggerType = "manual"
}
createView.Override = true
theme := huh.ThemeCharm()
// Step 1: Basic information
var basicGroup *huh.Group
if update {
basicGroup = huh.NewGroup(
huh.NewInput().
Title("Replication Policy Name").
Value(&createView.Name).
Validate(func(str string) error {
if str == "" {
return errors.New("name cannot be empty")
}
return nil
}),
huh.NewInput().
Title("Description").
Value(&createView.Description),
)
} else {
basicGroup = huh.NewGroup(
huh.NewInput().
Title("Replication Policy Name").
Value(&createView.Name).
Validate(func(str string) error {
if str == "" {
return errors.New("name cannot be empty")
}
return nil
}),
huh.NewInput().
Title("Description").
Value(&createView.Description),
huh.NewSelect[string]().
Title("Replication Mode").
Description("Choose whether to pull from or push to an external registry").
Options(
huh.NewOption("Pull (External → Harbor)", "Pull"),
huh.NewOption("Push (Harbor → External)", "Push"),
).
Value(&createView.ReplicationMode),
)
}
// Run the basic form first
basicForm := huh.NewForm(basicGroup).WithTheme(theme)
err := basicForm.Run()
if err != nil {
log.Fatal(err)
}
// Step 2: Create filter group based on selected mode
var filterGroup *huh.Group
if createView.ReplicationMode == "Pull" {
filterGroup = huh.NewGroup(
huh.NewSelect[string]().
Title("Resource Type").
Description("Type of artifacts to replicate").
Options(
huh.NewOption("Images only", "image"),
).
Value(&createView.ResourceFilter),
huh.NewInput().
Title("Name Filter").
Description("Repository name pattern (leave empty for all, supports wildcards like library/*)").
Value(&createView.NameFilter),
huh.NewSelect[string]().
Title("Tag Filter Type").
Description("How to filter by tags").
Options(
huh.NewOption("Include matching", "matches"),
huh.NewOption("Exclude matching", "excludes"),
).
Value(&createView.TagFilter),
huh.NewInput().
Title("Tag Pattern").
Description("Tag pattern (e.g., v*, latest, *-prod, leave empty to skip tag filtering)").
Value(&createView.TagPattern),
)
} else if createView.ReplicationMode == "Push" {
filterGroup = huh.NewGroup(
huh.NewSelect[string]().
Title("Resource Type").
Description("Type of artifacts to replicate").
Options(
huh.NewOption("All (Images & Artifacts)", ""),
huh.NewOption("Images only", "image"),
huh.NewOption("Artifacts only", "artifact"),
).
Value(&createView.ResourceFilter),
huh.NewInput().
Title("Name Filter").
Description("Repository name pattern (leave empty for all, supports wildcards like library/*)").
Value(&createView.NameFilter),
huh.NewSelect[string]().
Title("Tag Filter Type").
Description("How to filter by tags").
Options(
huh.NewOption("Include matching", "matches"),
huh.NewOption("Exclude matching", "excludes"),
).
Value(&createView.TagFilter),
huh.NewInput().
Title("Tag Pattern").
Description("Tag pattern (e.g., v*, latest, *-prod, leave empty to skip tag filtering)").
Value(&createView.TagPattern),
huh.NewSelect[string]().
Title("Label Filter Type").
Description("How to filter by labels").
Options(
huh.NewOption("Include matching", "matches"),
huh.NewOption("Exclude matching", "excludes"),
).
Value(&createView.LabelFilter),
huh.NewInput().
Title("Label Pattern").
Description("Label filter (e.g., 'environment=prod' or 'env=prod,version=1.0', leave empty to skip label filtering)").
Value(&createView.LabelPattern),
)
}
// Step 3: Create trigger group based on selected mode
var triggerGroup *huh.Group
if createView.ReplicationMode == "Push" {
triggerGroup = huh.NewGroup(
huh.NewSelect[string]().
Title("Trigger Mode").
Description("When should replication occur?").
Options(
huh.NewOption("Manual", "manual"),
huh.NewOption("Event Based", "event_based"),
huh.NewOption("Scheduled", "scheduled"),
).
Value(&createView.TriggerType),
)
} else if createView.ReplicationMode == "Pull" {
triggerGroup = huh.NewGroup(
huh.NewSelect[string]().
Title("Trigger Mode").
Description("When should replication occur?").
Options(
huh.NewOption("Manual", "manual"),
huh.NewOption("Scheduled", "scheduled"),
).
Value(&createView.TriggerType),
)
}
// Step 4: Advanced options
advancedGroup := huh.NewGroup(
huh.NewConfirm().
Title("Override").
Description("Replace artifacts on destination if they already exist").
Value(&createView.Override).
WithButtonAlignment(lipgloss.Left),
huh.NewConfirm().
Title("Replicate Deletion").
Description("Synchronize deletion operations between registries").
Value(&createView.ReplicateDeletion).
WithButtonAlignment(lipgloss.Left),
huh.NewConfirm().
Title("Copy By Chunk").
Description("Transfer artifacts in smaller chunks for better reliability").
Value(&createView.CopyByChunk).
WithButtonAlignment(lipgloss.Left),
huh.NewInput().
Title("Speed Limit").
Description("Maximum speed in KB/s (-1 = unlimited)").
Placeholder("-1").
Value(&createView.Speed).
Validate(func(s string) error {
if s == "" {
return nil
}
speed, err := strconv.ParseInt(s, 10, 32)
if err != nil {
return fmt.Errorf("speed must be a valid number")
}
if speed < -1 {
return fmt.Errorf("speed cannot be negative")
}
return nil
}),
huh.NewConfirm().
Title("Enabled").
Description("Activate replication policy after creation").
Value(&createView.Enabled).
WithButtonAlignment(lipgloss.Left),
)
// Run the remaining forms
restForm := huh.NewForm(filterGroup, triggerGroup, advancedGroup).WithTheme(theme)
err = restForm.Run()
if err != nil {
log.Fatal(err)
}
// Handle trigger-specific additional forms
if createView.TriggerType == "event_based" && createView.ReplicationMode == "Push" {
eventForm := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("Delete remote resources when locally deleted").
Description("When artifacts are deleted locally, also delete them on the remote registry").
Value(&createView.ReplicateDeletion).
WithButtonAlignment(lipgloss.Left),
),
).WithTheme(theme)
if err := eventForm.Run(); err != nil {
log.Fatal(err)
}
} else if createView.TriggerType == "scheduled" {
cronForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Cron String").
Description("Schedule using 6-field cron format: seconds minutes hours day-month month day-week").
Placeholder("0 0 0 * * *"). // At midnight (00:00:00) every day
Value(&createView.CronString).
Validate(func(s string) error {
if s == "" {
return errors.New("cron string cannot be empty for scheduled trigger")
}
// Basic validation for 6-field cron format
fields := strings.Fields(s)
if len(fields) != 6 {
return fmt.Errorf("cron must have exactly 6 fields (found %d): seconds minutes hours day-month month day-week", len(fields))
}
return nil
}),
),
).WithTheme(theme)
if err := cronForm.Run(); err != nil {
log.Fatal(err)
}
}
}

View File

@ -0,0 +1,62 @@
// 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 (
"fmt"
"os"
"strconv"
"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.WidthS},
{Title: "Name", Width: tablelist.WidthL},
{Title: "Enabled", Width: tablelist.WidthS},
{Title: "Source", Width: tablelist.Width3XL},
{Title: "Destination", Width: tablelist.Width3XL},
{Title: "Trigger Type", Width: tablelist.WidthM},
{Title: "Override", Width: tablelist.WidthS},
{Title: "Creation Time", Width: tablelist.WidthL},
{Title: "Last Modified", Width: tablelist.WidthL},
}
func ListPolicies(rpolicies []*models.ReplicationPolicy) {
var rows []table.Row
for _, rpolicy := range rpolicies {
createdTime, _ := utils.FormatCreatedTime(rpolicy.CreationTime.String())
modifledTime, _ := utils.FormatCreatedTime(rpolicy.UpdateTime.String())
rows = append(rows, table.Row{
strconv.FormatInt(rpolicy.ID, 10),
rpolicy.Name,
strconv.FormatBool(rpolicy.Enabled),
rpolicy.SrcRegistry.Name,
rpolicy.DestRegistry.Name,
rpolicy.Trigger.Type,
strconv.FormatBool(rpolicy.Override),
createdTime,
modifledTime,
})
}
m := tablelist.NewModel(columns, rows, len(rows))
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}

View File

@ -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 replication
import (
"fmt"
"os"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/views/base/selection"
)
func ReplicationPoliciesList(policies []*models.ReplicationPolicy, choice chan<- int64) {
policyNameIDsMap := make(map[string]int64, len(policies))
for _, p := range policies {
policyNameIDsMap[p.Name] = p.ID
}
itemsList := make([]list.Item, len(policies))
for i, p := range policies {
itemsList[i] = selection.Item(p.Name)
}
m := selection.NewModel(itemsList, "Replication Policy")
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 <- policyNameIDsMap[p.Choice]
}
}

View File

@ -0,0 +1,144 @@
// 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 view
import (
"fmt"
"os"
"strconv"
"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: "Property", Width: tablelist.WidthL},
{Title: "Value", Width: tablelist.Width3XL},
}
var filterColumns = []table.Column{
{Title: "Type", Width: tablelist.WidthM},
{Title: "Decoration", Width: tablelist.WidthM},
{Title: "Value", Width: tablelist.WidthXL},
}
var order = []string{
"ID",
"Name",
"Enabled",
"Source",
"Destination",
"Creation Time",
"Last Modified",
"Description",
"Trigger Type",
"Override",
"Replicate Deletion",
"Copy By Chunk",
"Speed",
}
func ViewPolicy(rpolicy *models.ReplicationPolicy) {
createdTime, _ := utils.FormatCreatedTime(rpolicy.CreationTime.String())
modifledTime, _ := utils.FormatCreatedTime(rpolicy.UpdateTime.String())
policyMap := map[string]string{
"ID": strconv.FormatInt(rpolicy.ID, 10),
"Name": rpolicy.Name,
"Source": getRegistryName(rpolicy.SrcRegistry),
"Destination": getRegistryName(rpolicy.DestRegistry),
"Trigger Type": rpolicy.Trigger.Type,
"Override": strconv.FormatBool(rpolicy.Override),
"Enabled": strconv.FormatBool(rpolicy.Enabled),
"Creation Time": createdTime,
"Last Modified": modifledTime,
"Description": rpolicy.Description,
"Replicate Deletion": strconv.FormatBool(rpolicy.ReplicateDeletion),
"Copy By Chunk": strconv.FormatBool(*rpolicy.CopyByChunk),
"Speed": strconv.FormatInt(int64(*rpolicy.Speed), 10) + " B/s",
}
var rows []table.Row
for _, key := range order {
rows = append(rows, table.Row{
key,
policyMap[key],
})
}
m := tablelist.NewModel(columns, rows, len(rows))
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
if len(rpolicy.Filters) > 0 {
fmt.Println("\nFilters:")
showFiltersTable(rpolicy.Filters)
} else {
fmt.Println("\nNo filters configured")
}
}
func showFiltersTable(filters []*models.ReplicationFilter) {
var filterRows []table.Row
for _, filter := range filters {
decoration := filter.Decoration
if decoration == "" {
decoration = "N/A"
}
value := formatFilterValue(filter.Value)
filterRows = append(filterRows, table.Row{
filter.Type,
decoration,
value,
})
}
m := tablelist.NewModel(filterColumns, filterRows, len(filterRows))
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
func formatFilterValue(value interface{}) string {
switch v := value.(type) {
case string:
return v
case []string:
return strings.Join(v, ", ")
case []interface{}:
var strValues []string
for _, item := range v {
strValues = append(strValues, fmt.Sprintf("%v", item))
}
return strings.Join(strValues, ", ")
default:
return fmt.Sprintf("%v", v)
}
}
func getRegistryName(registry *models.Registry) string {
if registry == nil {
return "Local"
}
return registry.Name
}

View File

@ -16,6 +16,7 @@ package create
import (
"errors"
"strconv"
"unicode"
"github.com/charmbracelet/huh"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
@ -58,8 +59,8 @@ func CreateRobotView(createView *CreateView) {
Title("Robot Name").
Value(&createView.Name).
Validate(func(str string) error {
if str == "" {
return errors.New("name cannot be empty")
if !isValidName(str) {
return errors.New("invalid name: must start with a letter and contain only letters, digits, hyphen or underscore, no uppercase letters")
}
return nil
}),
@ -108,3 +109,21 @@ func CreateRobotSecretView(name string, secret string) {
log.Fatal(err)
}
}
func isValidName(s string) bool {
if s == "" {
return false
}
if len(s) > 0 && !unicode.IsLetter(rune(s[0])) {
return false
}
for _, r := range s {
if unicode.IsUpper(r) {
return false
}
if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '-' && r != '_' {
return false
}
}
return true
}

View File

@ -49,7 +49,10 @@ func ListRobots(robots []*models.Robot) {
enabledStatus = views.GreenANSI + "Enabled" + views.ResetANSI
}
TotalPermissions := strconv.FormatInt(int64(len(robot.Permissions[0].Access)), 10)
var TotalPermissions int = 0
if len(robot.Permissions) > 0 {
TotalPermissions = len(robot.Permissions[0].Access)
}
if robot.ExpiresAt == -1 {
expires = "Never"
@ -61,7 +64,7 @@ func ListRobots(robots []*models.Robot) {
rows = append(rows, table.Row{
robot.Name,
enabledStatus,
TotalPermissions,
strconv.FormatInt(int64(TotalPermissions), 10),
createdTime,
expires,
string(robot.Description),

View File

@ -18,36 +18,92 @@ import (
"fmt"
"os"
"slices"
"sort"
"strings"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
"github.com/goharbor/harbor-cli/pkg/config"
config "github.com/goharbor/harbor-cli/pkg/config/robot"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views/base/selection"
"github.com/goharbor/harbor-cli/pkg/views/base/tablegrid"
)
const (
WidthResource = 20
WidthAction = 16
)
func NewRobotPermissionsGrid(kind string) *tablegrid.TableGrid {
const (
WidthResource = 20
WidthAction = 16
)
var resourceNames = []string{
"Accessory", "Artifact", "Artifact Addition", "Artifact Label",
"Export CVE", "Immutable Tag", "Label", "Log", "Member",
"Metadata", "Notification Policy", "Preheat Policy",
"Project", "Quota", "Repository", "Robot", "SBOM",
"Scan", "Scanner", "Tag", "Tag Retention",
}
var resourceNames []string
var columnLabels []string
var columnLabels = []string{
"Resource", "Create", "Delete", "List", "Pull", "Push", "Read", "Stop", "Update",
}
// Define resources, columns, and actions based on kind
if kind == "project" {
resourceNames = []string{
"Accessory", "Artifact", "Artifact Addition", "Artifact Label",
"Export CVE", "Immutable Tag", "Label", "Log", "Member",
"Metadata", "Notification Policy", "Preheat Policy",
"Project", "Quota", "Repository", "Robot", "SBOM",
"Scan", "Scanner", "Tag", "Tag Retention",
}
columnLabels = []string{
"Resource", "Create", "Delete",
"List", "Pull", "Push", "Read",
"Stop", "Update",
}
} else if kind == "system" {
resourceNames = []string{
"Audit Log", "Catalog", "Garbage Collection", "JobService Monitor",
"Label", "LDAP User", "Preheat Instance", "Project", "Purge Audit",
"Quota", "Registry", "Replication", "Replication Adapter", "Replication Policy",
"Robot", "Scan All", "Scanner", "Security Hub", "System Volumes",
"User", "User Group",
}
columnLabels = []string{
"Resource", "Create", "Delete",
"List", "Read", "Stop", "Update",
}
} else {
fmt.Printf("Invalid kind specified: %s, expected 'system' or 'project'\n", kind)
os.Exit(1)
}
func NewRobotPermissionsGrid() *tablegrid.TableGrid {
actions := make([]string, len(columnLabels)-1)
for i := 1; i < len(columnLabels); i++ {
actions[i-1] = strings.ToLower(columnLabels[i])
}
// System Robot Permissions Grid
// The disabled map corresponds to the permissions for each resource and action in the harbor UI.
// This shows which actions are available (✓) or unavailable (✗) for each resource:
// Resource | Create | Delete | List | Read | Stop | Update |
// ----------------------|--------|--------|------|------|------|--------|
// Audit Log | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ |
// Catalog | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ |
// Garbage Collection | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ |
// JobService Monitor | ✗ | ✗ | ✓ | ✗ | ✓ | ✗ |
// Label | ✓ | ✓ | ✗ | ✓ | ✗ | ✓ |
// LDAP User | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ |
// Preheat Instance | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
// Project | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ |
// Purge Audit | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ |
// Quota | ✗ | ✗ | ✓ | ✓ | ✗ | ✓ |
// Registry | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
// Replication | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ |
// Replication Adapter | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ |
// Replication Policy | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
// Robot | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ |
// Scan All | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ |
// Scanner | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
// Security Hub | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ |
// System Volumes | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ |
// User | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
// User Group | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
//
// -----------------------------------------------------------------------
//
// Project Robot Permissions Grid
// The disabled map corresponds to the permissions for each resource and action in the harbor UI.
// This shows which actions are available (✓) or unavailable (✗) for each resource:
//
@ -81,105 +137,66 @@ func NewRobotPermissionsGrid() *tablegrid.TableGrid {
// - Indices in the map: 1=Create, 2=Delete, 3=List, 4=Pull, 5=Push, 6=Read, 7=Stop, 8=Update
// This corresponds to the following. the map is autogenerated based on the available permissions
// and the resource names. The indices in the map correspond to the column indices in the table.
// disabled := map[int]map[int]bool{
// 0: {1: true, 2: true, 3: false, 4: true, 5: true, 6: true, 7: true, 8: true},
// 1: {1: false, 2: false, 3: false, 4: true, 5: true, 6: false, 7: true, 8: true},
// 2: {1: true, 2: true, 3: true, 4: true, 5: true, 6: false, 7: true, 8: true},
// 3: {1: false, 2: false, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true},
// 4: {1: false, 2: true, 3: true, 4: true, 5: true, 6: false, 7: true, 8: true},
// 5: {1: false, 2: false, 3: false, 4: true, 5: true, 6: true, 7: true, 8: false},
// 6: {1: false, 2: false, 3: false, 4: true, 5: true, 6: false, 7: true, 8: false},
// 7: {1: true, 2: true, 3: false, 4: true, 5: true, 6: true, 7: true, 8: true},
// 8: {1: false, 2: false, 3: false, 4: true, 5: true, 6: false, 7: true, 8: false},
// 9: {1: false, 2: false, 3: false, 4: true, 5: true, 6: false, 7: true, 8: false},
// 10: {1: false, 2: false, 3: false, 4: true, 5: true, 6: false, 7: true, 8: false},
// 11: {1: false, 2: false, 3: false, 4: true, 5: true, 6: false, 7: true, 8: false},
// 12: {1: true, 2: true, 3: true, 4: true, 5: true, 6: false, 7: false, 8: true},
// 13: {1: true, 2: true, 3: true, 4: true, 5: true, 6: false, 7: true, 8: true},
// 14: {1: true, 2: false, 3: false, 4: false, 5: false, 6: false, 7: true, 8: false},
// 15: {1: false, 2: false, 3: false, 4: true, 5: true, 6: false, 7: true, 8: true},
// 16: {1: false, 2: true, 3: true, 4: true, 5: true, 6: false, 7: false, 8: true},
// 17: {1: false, 2: true, 3: true, 4: true, 5: true, 6: false, 7: false, 8: true},
// 18: {1: false, 2: true, 3: true, 4: true, 5: true, 6: false, 7: true, 8: true},
// 19: {1: false, 2: false, 3: false, 4: true, 5: true, 6: true, 7: true, 8: true},
// 20: {1: false, 2: false, 3: false, 4: true, 5: true, 6: false, 7: true, 8: false},
// }
// Set up column widths
columnWidths := []int{WidthResource}
for range len(columnLabels) - 1 {
columnWidths = append(columnWidths, WidthAction)
}
availablePerms, err := config.GetAllAvailablePermissions()
// Get available permissions from API
perms, err := config.GetAllAvailablePermissions()
if err != nil {
fmt.Printf("Error fetching available permissions: %v\n", err)
os.Exit(1)
}
resourceMap := make(map[string]string)
var availablePerms map[string][]string
if kind == "project" {
availablePerms = perms.Project
} else if kind == "system" {
availablePerms = perms.System
} else {
fmt.Printf("invalid kind specified: %s, expected 'system' or 'project'\n", kind)
os.Exit(1)
}
// Map display names to API resource keys
resourceKeyMap := make(map[string]string)
for _, displayName := range resourceNames {
kebabName := utils.ToKebabCase(displayName)
resourceMap[kebabName] = displayName
resourceKeyMap[displayName] = kebabName
}
orderedKeys := make([]string, 0, len(availablePerms))
processed := make(map[string]bool)
// Create ordered list of resources that exist in API
orderedResources := []string{}
processedKeys := make(map[string]bool)
for _, name := range resourceNames {
kebabName := utils.ToKebabCase(name)
// First add resources in our predefined order that exist in API
for _, displayName := range resourceNames {
kebabName := resourceKeyMap[displayName]
if _, exists := availablePerms[kebabName]; exists {
orderedKeys = append(orderedKeys, kebabName)
processed[kebabName] = true
orderedResources = append(orderedResources, displayName)
processedKeys[kebabName] = true
}
}
var remainingKeys []string
for k := range availablePerms {
if !processed[k] {
remainingKeys = append(remainingKeys, k)
}
}
sort.Strings(remainingKeys)
orderedKeys = append(orderedKeys, remainingKeys...)
// Create disabled map for UI grid
disabled := make(map[int]map[int]bool)
for rowIdx := range orderedResources {
disabled[rowIdx] = make(map[int]bool)
actionIndices := map[string]int{
"create": 1,
"delete": 2,
"list": 3,
"pull": 4,
"push": 5,
"read": 6,
"stop": 7,
"update": 8,
}
// For each action column
for colIdx, action := range actions {
resourceKey := resourceKeyMap[orderedResources[rowIdx]]
validActions := availablePerms[resourceKey]
for i := range orderedKeys {
disabled[i] = make(map[int]bool)
for j := 1; j <= 8; j++ {
disabled[i][j] = false // Default to enabled
}
}
for i, resourceKey := range orderedKeys {
validActions := availablePerms[resourceKey]
for actionName, colIdx := range actionIndices {
if !slices.Contains(validActions, actionName) {
disabled[i][colIdx] = true
}
}
}
for i, key := range orderedKeys {
for j, action := range []string{"create", "delete", "list", "pull", "push", "read", "stop", "update"} {
if !slices.Contains(availablePerms[key], action) {
disabled[i][j+1] = true // Disable the action for this resource
} else {
disabled[i][j+1] = false // Enable the action for this resource
}
// Disable action if it's not available for this resource
disabled[rowIdx][colIdx+1] = !slices.Contains(validActions, action)
}
}
// Create the table grid
icons := &tablegrid.Icons{
Selected: "✅",
Unselected: "❌",
@ -187,7 +204,7 @@ func NewRobotPermissionsGrid() *tablegrid.TableGrid {
}
return tablegrid.New(tablegrid.Config{
RowLabels: resourceNames,
RowLabels: orderedResources,
ColLabels: columnLabels,
Disabled: disabled,
ColumnWidths: columnWidths,
@ -196,8 +213,8 @@ func NewRobotPermissionsGrid() *tablegrid.TableGrid {
})
}
func ListPermissions(perms *models.Permissions, ch chan<- []models.Permission) {
grid := NewRobotPermissionsGrid()
func ListPermissions(perms *models.Permissions, kind string, ch chan<- []models.Permission) {
grid := NewRobotPermissionsGrid(kind)
_, err := tea.NewProgram(grid, tea.WithAltScreen()).Run()
if err != nil {
fmt.Println("Error running program:", err)
@ -207,13 +224,12 @@ func ListPermissions(perms *models.Permissions, ch chan<- []models.Permission) {
data := grid.GetData()
selectedPerms := []models.Permission{}
for rowIdx, res := range resourceNames {
kebabResource := strings.ToLower(res)
kebabResource = strings.ReplaceAll(kebabResource, " ", "-")
for rowIdx, displayName := range grid.RowLabels {
kebabResource := utils.ToKebabCase(displayName)
for colIdx := 0; colIdx < len(columnLabels)-1; colIdx++ {
for colIdx := 0; colIdx < len(grid.ColLabels)-1; colIdx++ {
if data[rowIdx][colIdx] {
action := strings.ToLower(columnLabels[colIdx+1])
action := strings.ToLower(grid.ColLabels[colIdx+1])
selectedPerms = append(selectedPerms, models.Permission{
Resource: kebabResource,
Action: action,

View File

@ -51,8 +51,7 @@ type Access struct {
}
func UpdateRobotView(updateView *UpdateView) {
var duration string
duration = strconv.FormatInt(updateView.Duration, 10)
duration := strconv.FormatInt(updateView.Duration, 10)
theme := huh.ThemeCharm()
err := huh.NewForm(

View File

@ -23,7 +23,7 @@ import (
"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/config"
config "github.com/goharbor/harbor-cli/pkg/config/robot"
"github.com/goharbor/harbor-cli/pkg/utils"
"github.com/goharbor/harbor-cli/pkg/views"
"github.com/goharbor/harbor-cli/pkg/views/base/tablelist"
@ -38,7 +38,7 @@ var columns = []table.Column{
{Title: "Description", Width: tablelist.WidthXL},
}
var permissionsColumns = []table.Column{
var projectPermissionsColumns = []table.Column{
{Title: "Resource", Width: tablelist.WidthXL},
{Title: "Create", Width: tablelist.WidthS},
{Title: "Delete", Width: tablelist.WidthS},
@ -50,14 +50,32 @@ var permissionsColumns = []table.Column{
{Title: "Update", Width: tablelist.WidthS},
}
var resourceStrings = []string{
var systemPermissionsColumns = []table.Column{
{Title: "Resource", Width: tablelist.WidthXL},
{Title: "Create", Width: tablelist.WidthS},
{Title: "Delete", Width: tablelist.WidthS},
{Title: "List", Width: tablelist.WidthS},
{Title: "Read", Width: tablelist.WidthS},
{Title: "Stop", Width: tablelist.WidthS},
{Title: "Update", Width: tablelist.WidthS},
}
var projectResourceStrings = []string{
"Accessory", "Artifact", "Artifact Addition", "Artifact Label",
"Export CVE", "Immutable Tag", "Label", "Log", "Member",
"Metadata", "Notification Policy", "Preheat Policy",
"Project", "Quota", "Repository", "Robot", "SBOM",
"Project", "Quota", "Repository", "Robot Account", "SBOM",
"Scan", "Scanner", "Tag", "Tag Retention",
}
var systemResourceStrings = []string{
"Audit Log", "Catalog", "Garbage Collection", "JobService Monitor",
"Label", "LDAP User", "Preheat Instance", "Project", "Purge Audit",
"Quota", "Registry", "Replication", "Replication Adapter", "Replication Policy",
"Robot", "Scan All", "Scanner", "Security Hub", "System Volumes",
"User", "User Group",
}
func ViewRobot(robot *models.Robot) {
var rows []table.Row
var enabledStatus string
@ -94,19 +112,44 @@ func ViewRobot(robot *models.Robot) {
os.Exit(1)
}
fmt.Printf("\n%sRobot Permissions:%s\n\n", views.BoldANSI, views.ResetANSI)
var permissionsColumns []table.Column
var resourceStrings []string
var systemLevel bool
if robot.Level == "system" {
permissionsColumns = systemPermissionsColumns
resourceStrings = systemResourceStrings
systemLevel = true
fmt.Printf("%sSystem-level robot with access across projects%s\n\n", views.BoldANSI, views.ResetANSI)
} else {
permissionsColumns = projectPermissionsColumns
resourceStrings = projectResourceStrings
systemLevel = false
fmt.Printf("%sProject-level robot for project: %s%s\n\n", views.BoldANSI, robot.Permissions[0].Namespace, views.ResetANSI)
}
var permissionRows []table.Row
resActs := map[string][]string{}
for _, perm := range robot.Permissions {
for _, access := range perm.Access {
resActs[access.Resource] = append(resActs[access.Resource], access.Action)
}
}
availablePerms, err := config.GetAllAvailablePermissions()
perms, err := config.GetAllAvailablePermissions()
if err != nil {
fmt.Printf("Error fetching available permissions: %v\n", err)
os.Exit(1)
}
var availablePerms map[string][]string
if systemLevel {
availablePerms = perms.System
} else {
availablePerms = perms.Project
}
resourceMap := make(map[string]string)
for _, displayName := range resourceStrings {
@ -120,6 +163,73 @@ func ViewRobot(robot *models.Robot) {
continue
}
row := table.Row{displayName}
var actionsToCheck []string
if systemLevel {
actionsToCheck = []string{"create", "delete", "list", "read", "stop", "update"}
} else {
actionsToCheck = []string{"create", "delete", "list", "pull", "push", "read", "stop", "update"}
}
for _, action := range actionsToCheck {
if slices.Contains(availablePerms[kebabName], action) {
actions := resActs[kebabName]
if slices.Contains(actions, action) {
row = append(row, "✅")
} else {
row = append(row, "❌")
}
} else {
row = append(row, " ")
}
}
permissionRows = append(permissionRows, row)
}
if systemLevel && len(robot.Permissions) > 1 {
t := tablelist.NewModel(permissionsColumns, permissionRows, len(permissionRows))
if _, err := tea.NewProgram(t).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
fmt.Printf("\n%sProject-specific Permissions:%s\n", views.BoldANSI, views.ResetANSI)
for _, perm := range robot.Permissions {
if perm.Kind == "project" && perm.Namespace != "/" {
fmt.Printf("\n%sProject: %s%s\n\n", views.BoldANSI, perm.Namespace, views.ResetANSI)
projectRows := createProjectPermissionRows(perm, perms.Project)
pt := tablelist.NewModel(projectPermissionsColumns, projectRows, len(projectRows))
if _, err := tea.NewProgram(pt).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
}
} else {
t := tablelist.NewModel(permissionsColumns, permissionRows, len(permissionRows))
if _, err := tea.NewProgram(t).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
}
func createProjectPermissionRows(perm *models.RobotPermission, availablePerms map[string][]string) []table.Row {
var rows []table.Row
resActs := map[string][]string{}
for _, access := range perm.Access {
resActs[access.Resource] = append(resActs[access.Resource], access.Action)
}
for _, displayName := range projectResourceStrings {
kebabName := utils.ToKebabCase(displayName)
if _, exists := availablePerms[kebabName]; !exists {
continue
}
row := table.Row{displayName}
for _, action := range []string{"create", "delete", "list", "pull", "push", "read", "stop", "update"} {
if slices.Contains(availablePerms[kebabName], action) {
@ -133,15 +243,10 @@ func ViewRobot(robot *models.Robot) {
row = append(row, " ")
}
}
permissionRows = append(permissionRows, row)
rows = append(rows, row)
}
t := tablelist.NewModel(permissionsColumns, permissionRows, len(permissionRows))
if _, err := tea.NewProgram(t).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
return rows
}
func remainingTime(unixTimestamp int64) string {

View File

@ -32,5 +32,6 @@ var BaseStyle = lipgloss.NewStyle().
const (
GreenANSI = "\033[32m"
RedANSI = "\033[31m"
BoldANSI = "\033[1m"
ResetANSI = "\033[0m"
)