Compare commits
15 Commits
Author | SHA1 | Date |
---|---|---|
|
ff1485171c | |
|
e15bde4ba0 | |
|
3c8e6239a0 | |
|
6caed4d4f4 | |
|
18cb2da313 | |
|
85618bdd6b | |
|
0fbd8ecdea | |
|
c705221645 | |
|
cc2113c2dd | |
|
3304c2ea0f | |
|
fd38240593 | |
|
97fc4d6c64 | |
|
598b817a32 | |
|
95edbd6f8b | |
|
af41e84a14 |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
|
@ -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 – free‑form 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. Lower‑case 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 Read‑Only 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"
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
|
@ -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": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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:
|
||||
- "*"
|
|
@ -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": "/"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
14
go.mod
|
@ -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
28
go.sum
|
@ -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=
|
||||
|
|
BIN
harbor-cli
BIN
harbor-cli
Binary file not shown.
|
@ -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
|
||||
}
|
|
@ -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, ®istry.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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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{}
|
||||
}
|
|
@ -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{}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:]
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -32,5 +32,6 @@ var BaseStyle = lipgloss.NewStyle().
|
|||
const (
|
||||
GreenANSI = "\033[32m"
|
||||
RedANSI = "\033[31m"
|
||||
BoldANSI = "\033[1m"
|
||||
ResetANSI = "\033[0m"
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue