From 9283137c0bb19175e4a0f4b36d6de34e73a27317 Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Mon, 13 Apr 2026 00:15:48 +0530 Subject: [PATCH 1/7] feat(instance): add view subcommand for preheat provider instances Signed-off-by: Sypher845 --- cmd/harbor/root/instance/cmd.go | 1 + cmd/harbor/root/instance/delete.go | 5 +- cmd/harbor/root/instance/list.go | 2 +- cmd/harbor/root/instance/view.go | 77 ++++++++++++++++++++++++++++++ pkg/api/instance_handler.go | 31 +++++++++++- pkg/prompt/prompt.go | 34 +++++++++++-- pkg/views/instance/list/view.go | 22 ++++----- pkg/views/instance/select/view.go | 30 +++++++----- pkg/views/instance/view/view.go | 63 ++++++++++++++++++++++++ 9 files changed, 233 insertions(+), 32 deletions(-) create mode 100644 cmd/harbor/root/instance/view.go create mode 100644 pkg/views/instance/view/view.go diff --git a/cmd/harbor/root/instance/cmd.go b/cmd/harbor/root/instance/cmd.go index 864f706a1..1809fae5f 100644 --- a/cmd/harbor/root/instance/cmd.go +++ b/cmd/harbor/root/instance/cmd.go @@ -26,6 +26,7 @@ These instances represent external services such as Dragonfly or Kraken that hel CreateInstanceCommand(), DeleteInstanceCommand(), ListInstanceCommand(), + ViewInstanceCommand(), ) return cmd } diff --git a/cmd/harbor/root/instance/delete.go b/cmd/harbor/root/instance/delete.go index 87ec63598..a30d064d1 100644 --- a/cmd/harbor/root/instance/delete.go +++ b/cmd/harbor/root/instance/delete.go @@ -43,7 +43,10 @@ If no argument is provided, you will be prompted to select an instance from a li } else if len(args) > 0 { instanceName = args[0] } else { - instanceName = prompt.GetInstanceFromUser() + instanceName, err = prompt.GetInstanceNameFromUser() + if err != nil { + return fmt.Errorf("%v", err) + } } err = api.DeleteInstance(instanceName) if err != nil { diff --git a/cmd/harbor/root/instance/list.go b/cmd/harbor/root/instance/list.go index ec4071a7a..af2ad563c 100644 --- a/cmd/harbor/root/instance/list.go +++ b/cmd/harbor/root/instance/list.go @@ -42,7 +42,7 @@ This command provides an easy way to view all instances along with their details return fmt.Errorf("page size should be less than or equal to 100") } - instance, err := api.ListInstance(opts) + instance, err := api.ListAllInstance(opts) if err != nil { return fmt.Errorf("failed to get instance list: %v", err) diff --git a/cmd/harbor/root/instance/view.go b/cmd/harbor/root/instance/view.go new file mode 100644 index 000000000..655682104 --- /dev/null +++ b/cmd/harbor/root/instance/view.go @@ -0,0 +1,77 @@ +// 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 instance + +import ( + "fmt" + + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/preheat" + "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/instance/view" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func ViewInstanceCommand() *cobra.Command { + var isID bool + cmd := &cobra.Command{ + Use: "view [NAME|ID]", + Short: "get preheat provider instance by name or id", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + var instanceName string + var instance *preheat.GetInstanceOK + + if len(args) > 0 { + log.Debugf("Instance name provided: %s", args[0]) + instanceName = args[0] + } else { + log.Debug("No instance name provided, prompting user") + instanceName, err = prompt.GetInstanceNameFromUser() + if err != nil { + return fmt.Errorf("failed to get instance name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + log.Debugf("Fetching instance: %s", instanceName) + instance, err = api.GetInstance(instanceName, isID) + if err != nil { + if utils.ParseHarborErrorCode(err) == "404" { + return fmt.Errorf("instance %s does not exist", instanceName) + } + return fmt.Errorf("failed to get instance: %v", utils.ParseHarborErrorMsg(err)) + } + + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + err = utils.PrintFormat(instance, FormatFlag) + if err != nil { + return err + } + } else { + view.ViewInstance(instance.Payload) + } + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&isID, "id", false, "Get instance by id") + + return cmd +} diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index be0d9f6f2..8ddc4caf7 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -15,6 +15,7 @@ package api import ( "fmt" + "strconv" "strings" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/preheat" @@ -55,7 +56,7 @@ func DeleteInstance(instanceName string) error { return nil } -func ListInstance(opts ...ListFlags) (*preheat.ListInstancesOK, error) { +func ListAllInstance(opts ...ListFlags) (*preheat.ListInstancesOK, error) { ctx, client, err := utils.ContextWithClient() if err != nil { return nil, err @@ -82,7 +83,7 @@ func ListInstance(opts ...ListFlags) (*preheat.ListInstancesOK, error) { } func GetInstanceNameByID(id int64) (string, error) { - instances, err := ListInstance() + instances, err := ListAllInstance() if err != nil { return "", err } @@ -95,3 +96,29 @@ func GetInstanceNameByID(id int64) (string, error) { return "", fmt.Errorf("no instance found with ID: %v", id) } + +func GetInstance(instanceNameOrID string, useInstanceID bool) (*preheat.GetInstanceOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + if useInstanceID { + instanceID, err := strconv.ParseInt(instanceNameOrID, 10, 64) + if err != nil { + return nil, err + } + instanceNameOrID, err = GetInstanceNameByID(instanceID) + if err != nil { + return nil, err + } + } + + response, err := client.Preheat.GetInstance(ctx, &preheat.GetInstanceParams{ + PreheatInstanceName: instanceNameOrID, + }) + if err != nil { + return nil, err + } + + return response, nil +} diff --git a/pkg/prompt/prompt.go b/pkg/prompt/prompt.go index 2ed2e2890..8c05e6e11 100644 --- a/pkg/prompt/prompt.go +++ b/pkg/prompt/prompt.go @@ -266,15 +266,39 @@ func GetLabelIdFromUser(opts api.ListFlags) (int64, error) { return res.id, res.err } -func GetInstanceFromUser() string { - instanceName := make(chan string) +func GetInstanceNameFromUser() (string, error) { + type result struct { + name string + err error + } + resultChan := make(chan result) go func() { - response, _ := api.ListInstance() - instview.InstanceList(response.Payload, instanceName) + response, err := api.ListAllInstance() + if err != nil { + resultChan <- result{"", err} + return + } + + if len(response.Payload) == 0 { + resultChan <- result{"", errors.New("no instances found")} + return + } + + name, err := instview.InstanceList(response.Payload) + if err != nil { + if err == instview.ErrUserAborted { + resultChan <- result{"", errors.New("user aborted instance selection")} + } else { + resultChan <- result{"", fmt.Errorf("error during instance selection: %w", err)} + } + return + } + resultChan <- result{name, nil} }() - return <-instanceName + res := <-resultChan + return res.name, res.err } func GetQuotaIDFromUser() int64 { diff --git a/pkg/views/instance/list/view.go b/pkg/views/instance/list/view.go index 68f9371fc..e31093803 100644 --- a/pkg/views/instance/list/view.go +++ b/pkg/views/instance/list/view.go @@ -25,17 +25,17 @@ import ( ) var columns = []table.Column{ - {Title: "ID", Width: 6}, - {Title: "Name", Width: 12}, - {Title: "Provider", Width: 12}, - {Title: "Endpoint", Width: 24}, - {Title: "Status", Width: 12}, - {Title: "Auth Mode", Width: 10}, - {Title: "Description", Width: 12}, - {Title: "Default", Width: 8}, - {Title: "Insecure", Width: 8}, - {Title: "Enabled", Width: 8}, - {Title: "Setup Timestamp", Width: 20}, + {Title: "ID", Width: tablelist.WidthXS}, + {Title: "Name", Width: tablelist.WidthM}, + {Title: "Provider", Width: tablelist.WidthM}, + {Title: "Endpoint", Width: tablelist.WidthXXL}, + {Title: "Status", Width: tablelist.WidthM}, + {Title: "Auth Mode", Width: tablelist.WidthM}, + {Title: "Description", Width: tablelist.WidthM}, + {Title: "Default", Width: tablelist.WidthS}, + {Title: "Insecure", Width: tablelist.WidthS}, + {Title: "Enabled", Width: tablelist.WidthS}, + {Title: "Setup Timestamp", Width: tablelist.WidthXL}, } func ListInstance(instance []*models.Instance) { diff --git a/pkg/views/instance/select/view.go b/pkg/views/instance/select/view.go index 5b9bb330f..f7c7f1c8b 100644 --- a/pkg/views/instance/select/view.go +++ b/pkg/views/instance/select/view.go @@ -14,6 +14,7 @@ package instance import ( + "errors" "fmt" "os" @@ -23,26 +24,31 @@ import ( "github.com/goharbor/harbor-cli/pkg/views/base/selection" ) -func InstanceList(instance []*models.Instance, choice chan<- string) { - itemsList := make([]list.Item, len(instance)) +var ErrUserAborted = errors.New("user aborted selection") - items := map[string]string{} - - for i, r := range instance { - items[r.Name] = r.Name - itemsList[i] = selection.Item(r.Name) +func InstanceList(instances []*models.Instance) (string, error) { + items := make([]list.Item, len(instances)) + for i, instance := range instances { + items[i] = selection.Item(instance.Name) } - m := selection.NewModel(itemsList, "Instance") - - p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() + m := selection.NewModel(items, "Instance") + p, err := tea.NewProgram(m).Run() if err != nil { fmt.Println("Error running program:", err) os.Exit(1) } - if p, ok := p.(selection.Model); ok { - choice <- items[p.Choice] + if model, ok := p.(selection.Model); ok { + if model.Aborted { + return "", ErrUserAborted + } + if model.Choice == "" { + return "", errors.New("no instance selected") + } + return model.Choice, nil } + + return "", errors.New("unexpected program result") } diff --git a/pkg/views/instance/view/view.go b/pkg/views/instance/view/view.go new file mode 100644 index 000000000..025c4d0dd --- /dev/null +++ b/pkg/views/instance/view/view.go @@ -0,0 +1,63 @@ +// 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" + "time" + + "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/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "ID", Width: tablelist.WidthXS}, + {Title: "Name", Width: tablelist.WidthM}, + {Title: "Provider", Width: tablelist.WidthM}, + {Title: "Endpoint", Width: tablelist.WidthXXL}, + {Title: "Status", Width: tablelist.WidthM}, + {Title: "Auth Mode", Width: tablelist.WidthM}, + {Title: "Description", Width: tablelist.WidthM}, + {Title: "Default", Width: tablelist.WidthS}, + {Title: "Insecure", Width: tablelist.WidthS}, + {Title: "Enabled", Width: tablelist.WidthS}, + {Title: "Setup Timestamp", Width: tablelist.WidthXL}, +} + +func ViewInstance(instance *models.Instance) { + var rows []table.Row + rows = append(rows, table.Row{ + fmt.Sprintf("%d", instance.ID), + instance.Name, + instance.Vendor, + instance.Endpoint, + instance.Status, + instance.AuthMode, + instance.Description, + fmt.Sprintf("%t", instance.Default), + fmt.Sprintf("%t", instance.Insecure), + fmt.Sprintf("%t", instance.Enabled), + time.Unix(instance.SetupTimestamp, 0).Format("2006-01-02 15:04:05"), + }) + + m := tablelist.NewModel(columns, rows, len(rows)) + + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} From 5c55a6bca4b3bf6c4ec4bac21296d709221bba96 Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Mon, 13 Apr 2026 00:17:22 +0530 Subject: [PATCH 2/7] doc(instance): add view subcommand docs Signed-off-by: Sypher845 --- doc/cli-docs/harbor-instance-view.md | 33 ++++++++++++++++++++ doc/cli-docs/harbor-instance.md | 1 + doc/man-docs/man1/harbor-instance-view.1 | 39 ++++++++++++++++++++++++ doc/man-docs/man1/harbor-instance.1 | 2 +- 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 doc/cli-docs/harbor-instance-view.md create mode 100644 doc/man-docs/man1/harbor-instance-view.1 diff --git a/doc/cli-docs/harbor-instance-view.md b/doc/cli-docs/harbor-instance-view.md new file mode 100644 index 000000000..b420bda58 --- /dev/null +++ b/doc/cli-docs/harbor-instance-view.md @@ -0,0 +1,33 @@ +--- +title: harbor instance view +weight: 40 +--- +## harbor instance view + +### Description + +##### get preheat provider instance by name or id + +```sh +harbor instance view [NAME|ID] [flags] +``` + +### Options + +```sh + -h, --help help for view + --id Get instance by id +``` + +### 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 instance](harbor-instance.md) - Manage preheat provider instances in Harbor + diff --git a/doc/cli-docs/harbor-instance.md b/doc/cli-docs/harbor-instance.md index deb7b8d49..8b3f7dbda 100644 --- a/doc/cli-docs/harbor-instance.md +++ b/doc/cli-docs/harbor-instance.md @@ -33,4 +33,5 @@ These instances represent external services such as Dragonfly or Kraken that hel * [harbor instance create](harbor-instance-create.md) - Create a new preheat provider instance in Harbor * [harbor instance delete](harbor-instance-delete.md) - Delete a preheat provider instance by its name or ID * [harbor instance list](harbor-instance-list.md) - List all preheat provider instances in Harbor +* [harbor instance view](harbor-instance-view.md) - get preheat provider instance by name or id diff --git a/doc/man-docs/man1/harbor-instance-view.1 b/doc/man-docs/man1/harbor-instance-view.1 new file mode 100644 index 000000000..a2555b702 --- /dev/null +++ b/doc/man-docs/man1/harbor-instance-view.1 @@ -0,0 +1,39 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-instance-view - get preheat provider instance by name or id + + +.SH SYNOPSIS +\fBharbor instance view [NAME|ID] [flags]\fP + + +.SH DESCRIPTION +get preheat provider instance by name or id + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for view + +.PP +\fB--id\fP[=false] + Get instance by id + + +.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-instance(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-instance.1 b/doc/man-docs/man1/harbor-instance.1 index 16f61281d..e6a54840b 100644 --- a/doc/man-docs/man1/harbor-instance.1 +++ b/doc/man-docs/man1/harbor-instance.1 @@ -33,4 +33,4 @@ These instances represent external services such as Dragonfly or Kraken that hel .SH SEE ALSO -\fBharbor(1)\fP, \fBharbor-instance-create(1)\fP, \fBharbor-instance-delete(1)\fP, \fBharbor-instance-list(1)\fP \ No newline at end of file +\fBharbor(1)\fP, \fBharbor-instance-create(1)\fP, \fBharbor-instance-delete(1)\fP, \fBharbor-instance-list(1)\fP, \fBharbor-instance-view(1)\fP \ No newline at end of file From 08692abeeae3638efce8f2c2d2ea80902898966e Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Tue, 14 Apr 2026 12:35:04 +0530 Subject: [PATCH 3/7] fix(instance): fix auth credential persistence and add auth flags for create command Signed-off-by: Sypher845 --- cmd/harbor/root/instance/create.go | 69 ++++++++++++++++------ doc/cli-docs/harbor-instance-create.md | 19 +++--- doc/man-docs/man1/harbor-instance-create.1 | 24 ++++++-- pkg/views/instance/create/view.go | 33 ++++++----- 4 files changed, 99 insertions(+), 46 deletions(-) diff --git a/cmd/harbor/root/instance/create.go b/cmd/harbor/root/instance/create.go index 2b51d9cbc..1ff2027d6 100644 --- a/cmd/harbor/root/instance/create.go +++ b/cmd/harbor/root/instance/create.go @@ -24,6 +24,7 @@ import ( func CreateInstanceCommand() *cobra.Command { var opts create.CreateView + var authUsername, authPassword, authToken string cmd := &cobra.Command{ Use: "create", @@ -35,16 +36,7 @@ You will need to provide the instance's name, vendor, endpoint, and optionally o Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { var err error - createView := &create.CreateView{ - Name: opts.Name, - Vendor: opts.Vendor, - Description: opts.Description, - Endpoint: opts.Endpoint, - Insecure: opts.Insecure, - Enabled: opts.Enabled, - AuthMode: opts.AuthMode, - AuthInfo: opts.AuthInfo, - } + var instanceName string if opts.Name != "" && opts.Vendor != "" && opts.Endpoint != "" { formattedEndpoint := utils.FormatUrl(opts.Endpoint) @@ -52,26 +44,65 @@ You will need to provide the instance's name, vendor, endpoint, and optionally o return err } opts.Endpoint = formattedEndpoint + + switch opts.AuthMode { + case "BASIC": + if authUsername == "" || authPassword == "" { + return fmt.Errorf("username and password are required when authmode is BASIC. Use --auth-username and --auth-password flags") + } + opts.AuthInfo = map[string]string{ + "username": authUsername, + "password": authPassword, + } + case "OAUTH": + if authToken == "" { + return fmt.Errorf("token is required when authmode is OAUTH. Use --auth-token flag") + } + opts.AuthInfo = map[string]string{ + "token": authToken, + } + case "NONE": + // Auth credentials are ignored when authmode is NONE + default: + return fmt.Errorf("invalid authmode '%s'. Valid options: NONE, BASIC, OAUTH", opts.AuthMode) + } + err = api.CreateInstance(opts) + instanceName = opts.Name } else { + createView := &create.CreateView{ + Name: opts.Name, + Vendor: opts.Vendor, + Description: opts.Description, + Endpoint: opts.Endpoint, + Insecure: opts.Insecure, + Enabled: opts.Enabled, + AuthMode: opts.AuthMode, + } err = createInstanceView(createView) + instanceName = createView.Name } if err != nil { - return fmt.Errorf("failed to create instance: %v", err) + return fmt.Errorf("failed to create instance: %v", utils.ParseHarborErrorMsg(err)) } + + fmt.Printf("Instance '%s' created successfully\n", instanceName) return nil }, } flags := cmd.Flags() flags.StringVarP(&opts.Name, "name", "n", "", "Name of the instance") - flags.StringVarP(&opts.Vendor, "provider", "p", "", "Provider for the instance") - flags.StringVarP(&opts.Endpoint, "url", "u", "", "URL for the instance") - flags.StringVarP(&opts.Description, "description", "", "", "Description of the instance") - flags.BoolVarP(&opts.Insecure, "insecure", "i", true, "Whether or not the certificate will be verified when Harbor tries to access the server") - flags.BoolVarP(&opts.Enabled, "enable", "", true, "Whether it is enabled or not") - flags.StringVarP(&opts.AuthMode, "authmode", "a", "NONE", "Choosing different types of authentication method") + flags.StringVarP(&opts.Vendor, "provider", "p", "", "Provider for the instance (e.g. dragonfly, kraken)") + flags.StringVarP(&opts.Endpoint, "url", "u", "", "Endpoint URL for the instance") + flags.StringVarP(&opts.Description, "description", "d", "", "Description of the instance") + flags.BoolVarP(&opts.Insecure, "insecure", "", false, "Whether or not the certificate will be verified when Harbor tries to access the server") + flags.BoolVarP(&opts.Enabled, "enable", "", true, "Whether the instance is enabled or not") + flags.StringVarP(&opts.AuthMode, "authmode", "a", "NONE", "Authentication mode (NONE, BASIC, OAUTH)") + flags.StringVar(&authUsername, "auth-username", "", "Username for BASIC authentication") + flags.StringVar(&authPassword, "auth-password", "", "Password for BASIC authentication") + flags.StringVar(&authToken, "auth-token", "", "Token for OAUTH authentication") return cmd } @@ -81,6 +112,8 @@ func createInstanceView(createView *create.CreateView) error { createView = &create.CreateView{} } - create.CreateInstanceView(createView) + if err := create.CreateInstanceView(createView); err != nil { + return err + } return api.CreateInstance(*createView) } diff --git a/doc/cli-docs/harbor-instance-create.md b/doc/cli-docs/harbor-instance-create.md index 7b03ff6ae..0d9fa8503 100644 --- a/doc/cli-docs/harbor-instance-create.md +++ b/doc/cli-docs/harbor-instance-create.md @@ -27,14 +27,17 @@ harbor instance create [flags] ### Options ```sh - -a, --authmode string Choosing different types of authentication method (default "NONE") - --description string Description of the instance - --enable Whether it is enabled or not (default true) - -h, --help help for create - -i, --insecure Whether or not the certificate will be verified when Harbor tries to access the server (default true) - -n, --name string Name of the instance - -p, --provider string Provider for the instance - -u, --url string URL for the instance + --auth-password string Password for BASIC authentication + --auth-token string Token for OAUTH authentication + --auth-username string Username for BASIC authentication + -a, --authmode string Authentication mode (NONE, BASIC, OAUTH) (default "NONE") + -d, --description string Description of the instance + --enable Whether the instance is enabled or not (default true) + -h, --help help for create + --insecure Whether or not the certificate will be verified when Harbor tries to access the server + -n, --name string Name of the instance + -p, --provider string Provider for the instance (e.g. dragonfly, kraken) + -u, --url string Endpoint URL for the instance ``` ### Options inherited from parent commands diff --git a/doc/man-docs/man1/harbor-instance-create.1 b/doc/man-docs/man1/harbor-instance-create.1 index 6477c38e4..43380007c 100644 --- a/doc/man-docs/man1/harbor-instance-create.1 +++ b/doc/man-docs/man1/harbor-instance-create.1 @@ -16,23 +16,35 @@ You will need to provide the instance's name, vendor, endpoint, and optionally o .SH OPTIONS +\fB--auth-password\fP="" + Password for BASIC authentication + +.PP +\fB--auth-token\fP="" + Token for OAUTH authentication + +.PP +\fB--auth-username\fP="" + Username for BASIC authentication + +.PP \fB-a\fP, \fB--authmode\fP="NONE" - Choosing different types of authentication method + Authentication mode (NONE, BASIC, OAUTH) .PP -\fB--description\fP="" +\fB-d\fP, \fB--description\fP="" Description of the instance .PP \fB--enable\fP[=true] - Whether it is enabled or not + Whether the instance is enabled or not .PP \fB-h\fP, \fB--help\fP[=false] help for create .PP -\fB-i\fP, \fB--insecure\fP[=true] +\fB--insecure\fP[=false] Whether or not the certificate will be verified when Harbor tries to access the server .PP @@ -41,11 +53,11 @@ You will need to provide the instance's name, vendor, endpoint, and optionally o .PP \fB-p\fP, \fB--provider\fP="" - Provider for the instance + Provider for the instance (e.g. dragonfly, kraken) .PP \fB-u\fP, \fB--url\fP="" - URL for the instance + Endpoint URL for the instance .SH OPTIONS INHERITED FROM PARENT COMMANDS diff --git a/pkg/views/instance/create/view.go b/pkg/views/instance/create/view.go index 13dc896e3..c35a7abee 100644 --- a/pkg/views/instance/create/view.go +++ b/pkg/views/instance/create/view.go @@ -19,7 +19,6 @@ import ( "github.com/charmbracelet/huh" "github.com/goharbor/harbor-cli/pkg/utils" - log "github.com/sirupsen/logrus" ) type CreateView struct { @@ -33,17 +32,8 @@ type CreateView struct { Insecure bool } -func CreateInstanceView(createView *CreateView) { - cv := CreateView{ - AuthInfo: map[string]string{ - "username": "", - "password": "", - "token": "", - }, - } - username := cv.AuthInfo["username"] - password := cv.AuthInfo["password"] - token := cv.AuthInfo["token"] +func CreateInstanceView(createView *CreateView) error { + var username, password, token string theme := huh.ThemeCharm() err := huh.NewForm( @@ -90,7 +80,7 @@ func CreateInstanceView(createView *CreateView) { Affirmative("yes"). Negative("no"), huh.NewConfirm(). - Title("Verify Cert"). + Title("Skip Certificate Verification"). Value(&createView.Insecure). Affirmative("yes"). Negative("no"), @@ -151,6 +141,21 @@ func CreateInstanceView(createView *CreateView) { ).WithTheme(theme).Run() if err != nil { - log.Fatal(err) + return err + } + + switch createView.AuthMode { + case "BASIC": + createView.AuthInfo = map[string]string{ + "username": username, + "password": password, + } + case "OAUTH": + createView.AuthInfo = map[string]string{ + "token": token, + } } + + return nil } + From 556f8e9df6a2a9c07daf70c1e5763457c2240ed5 Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Thu, 16 Apr 2026 01:30:31 +0530 Subject: [PATCH 4/7] feat(instance): add update subcommand for preheat provider instances Signed-off-by: Sypher845 --- cmd/harbor/root/instance/cmd.go | 1 + cmd/harbor/root/instance/update.go | 199 +++++++++++++++++++++++++++++ pkg/api/instance_handler.go | 20 +++ pkg/views/instance/create/view.go | 7 +- pkg/views/instance/update/view.go | 166 ++++++++++++++++++++++++ 5 files changed, 389 insertions(+), 4 deletions(-) create mode 100644 cmd/harbor/root/instance/update.go create mode 100644 pkg/views/instance/update/view.go diff --git a/cmd/harbor/root/instance/cmd.go b/cmd/harbor/root/instance/cmd.go index 1809fae5f..ebebbd7b3 100644 --- a/cmd/harbor/root/instance/cmd.go +++ b/cmd/harbor/root/instance/cmd.go @@ -26,6 +26,7 @@ These instances represent external services such as Dragonfly or Kraken that hel CreateInstanceCommand(), DeleteInstanceCommand(), ListInstanceCommand(), + UpdateInstanceCommand(), ViewInstanceCommand(), ) return cmd diff --git a/cmd/harbor/root/instance/update.go b/cmd/harbor/root/instance/update.go new file mode 100644 index 000000000..b7b20b96a --- /dev/null +++ b/cmd/harbor/root/instance/update.go @@ -0,0 +1,199 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (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 instance + +import ( + "fmt" + "strings" + + "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/utils" + viewupdate "github.com/goharbor/harbor-cli/pkg/views/instance/update" + "github.com/spf13/cobra" +) + +type updateOptions struct { + Name string + Description string + Endpoint string + AuthMode string + Enabled bool + Insecure bool + AuthUsername string + AuthPassword string + AuthToken string +} + +func UpdateInstanceCommand() *cobra.Command { + var opts updateOptions + var isID bool + + cmd := &cobra.Command{ + Use: "update [NAME|ID]", + Short: "Update a preheat provider instance in Harbor", + Long: `Update a preheat provider instance in Harbor by name or ID. If no update +flags are provided, the command opens an interactive update form.`, + Example: ` harbor-cli instance update my-instance --description "Updated preheat instance" + harbor-cli instance update 1 --id --enable=false + harbor-cli instance update my-instance --authmode BASIC --auth-username admin --auth-password Harbor12345`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + var instanceName string + + if isID && len(args) == 0 { + return fmt.Errorf("instance ID must be provided when using --id") + } + + if len(args) > 0 { + instanceName = args[0] + } else { + instanceName, err = prompt.GetInstanceNameFromUser() + if err != nil { + return fmt.Errorf("failed to get instance name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + instance, err := getInstanceForUpdate(instanceName, isID) + if err != nil { + return err + } + originalName := instance.Name + + if hasUpdateFlagChanges(cmd) { + err = applyUpdateFlags(cmd, instance, opts) + } else { + err = viewupdate.UpdateInstanceView(instance) + } + if err != nil { + return err + } + + err = api.UpdateInstance(originalName, *instance) + if err != nil { + return fmt.Errorf("failed to update instance: %v", utils.ParseHarborErrorMsg(err)) + } + + fmt.Printf("Instance '%s' updated successfully\n", instance.Name) + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&isID, "id", false, "Get instance by id") + flags.StringVarP(&opts.Name, "name", "n", "", "New name for the instance") + flags.StringVarP(&opts.Endpoint, "url", "u", "", "Endpoint URL for the instance") + flags.StringVarP(&opts.Description, "description", "d", "", "Description of the instance") + flags.BoolVarP(&opts.Insecure, "insecure", "", false, "Whether or not the certificate will be verified when Harbor tries to access the server") + flags.BoolVarP(&opts.Enabled, "enable", "", false, "Whether the instance is enabled or not") + flags.StringVarP(&opts.AuthMode, "authmode", "a", "", "Authentication mode (NONE, BASIC, OAUTH)") + flags.StringVar(&opts.AuthUsername, "auth-username", "", "Username for BASIC authentication") + flags.StringVar(&opts.AuthPassword, "auth-password", "", "Password for BASIC authentication") + flags.StringVar(&opts.AuthToken, "auth-token", "", "Token for OAUTH authentication") + + return cmd +} + +func getInstanceForUpdate(instanceName string, isID bool) (*models.Instance, error) { + instance, err := api.GetInstance(instanceName, isID) + if err != nil { + if utils.ParseHarborErrorCode(err) == "404" { + return nil, fmt.Errorf("instance %s does not exist", instanceName) + } + return nil, fmt.Errorf("failed to get instance: %v", utils.ParseHarborErrorMsg(err)) + } + + if instance == nil || instance.Payload == nil { + return nil, fmt.Errorf("failed to get instance: empty response") + } + + return instance.Payload, nil +} + +func hasUpdateFlagChanges(cmd *cobra.Command) bool { + flags := cmd.Flags() + return flags.Changed("name") || + flags.Changed("url") || + flags.Changed("description") || + flags.Changed("insecure") || + flags.Changed("enable") || + flags.Changed("authmode") || + flags.Changed("auth-username") || + flags.Changed("auth-password") || + flags.Changed("auth-token") +} + +func applyUpdateFlags(cmd *cobra.Command, instance *models.Instance, opts updateOptions) error { + flags := cmd.Flags() + + if flags.Changed("name") { + if strings.TrimSpace(opts.Name) == "" { + return fmt.Errorf("name cannot be empty or only spaces") + } + instance.Name = strings.TrimSpace(opts.Name) + } + if flags.Changed("url") { + formattedURL := utils.FormatUrl(opts.Endpoint) + if err := utils.ValidateURL(formattedURL); err != nil { + return err + } + instance.Endpoint = formattedURL + } + if flags.Changed("description") { + instance.Description = opts.Description + } + if flags.Changed("insecure") { + instance.Insecure = opts.Insecure + } + if flags.Changed("enable") { + instance.Enabled = opts.Enabled + } + + if flags.Changed("auth-username") || flags.Changed("auth-password") || flags.Changed("auth-token") { + if !flags.Changed("authmode") { + return fmt.Errorf("authmode is required when updating auth credentials") + } + } + + if !flags.Changed("authmode") { + return nil + } + + instance.AuthMode = strings.ToUpper(strings.TrimSpace(opts.AuthMode)) + + switch instance.AuthMode { + case "BASIC": + if strings.TrimSpace(opts.AuthUsername) == "" || strings.TrimSpace(opts.AuthPassword) == "" { + return fmt.Errorf("username and password are required when authmode is BASIC. Use --auth-username and --auth-password flags") + } + instance.AuthInfo = map[string]string{ + "username": strings.TrimSpace(opts.AuthUsername), + "password": opts.AuthPassword, + } + case "OAUTH": + if strings.TrimSpace(opts.AuthToken) == "" { + return fmt.Errorf("token is required when authmode is OAUTH. Use --auth-token flag") + } + instance.AuthInfo = map[string]string{ + "token": strings.TrimSpace(opts.AuthToken), + } + case "NONE": + instance.AuthInfo = nil + default: + return fmt.Errorf("invalid authmode '%s'. Valid options: NONE, BASIC, OAUTH", instance.AuthMode) + } + + return nil +} diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index 8ddc4caf7..24dd8ed55 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -56,6 +56,26 @@ func DeleteInstance(instanceName string) error { return nil } +func UpdateInstance(instanceName string, instance models.Instance) error { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return err + } + + instance.Endpoint = strings.TrimSpace(instance.Endpoint) + + _, err = client.Preheat.UpdateInstance(ctx, &preheat.UpdateInstanceParams{ + PreheatInstanceName: instanceName, + Instance: &instance, + }) + if err != nil { + return err + } + + log.Infof("Instance %s updated", instance.Name) + return nil +} + func ListAllInstance(opts ...ListFlags) (*preheat.ListInstancesOK, error) { ctx, client, err := utils.ContextWithClient() if err != nil { diff --git a/pkg/views/instance/create/view.go b/pkg/views/instance/create/view.go index c35a7abee..56b4d1e45 100644 --- a/pkg/views/instance/create/view.go +++ b/pkg/views/instance/create/view.go @@ -67,11 +67,11 @@ func CreateInstanceView(createView *CreateView) error { if strings.TrimSpace(str) == "" { return errors.New("endpoint cannot be empty or only spaces") } - formattedUrl := utils.FormatUrl(str) - if err := utils.ValidateURL(formattedUrl); err != nil { + formattedURL := utils.FormatUrl(str) + if err := utils.ValidateURL(formattedURL); err != nil { return err } - createView.Endpoint = formattedUrl + createView.Endpoint = formattedURL return nil }), huh.NewConfirm(). @@ -158,4 +158,3 @@ func CreateInstanceView(createView *CreateView) error { return nil } - diff --git a/pkg/views/instance/update/view.go b/pkg/views/instance/update/view.go new file mode 100644 index 000000000..6283d9d2d --- /dev/null +++ b/pkg/views/instance/update/view.go @@ -0,0 +1,166 @@ +// 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 update + +import ( + "errors" + "strings" + + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" +) + +func UpdateInstanceView(instance *models.Instance) error { + theme := huh.ThemeCharm() + + authUsername := "" + authPassword := "" + authToken := "" + + if instance.AuthInfo != nil { + switch strings.ToUpper(instance.AuthMode) { + case "BASIC": + authUsername = instance.AuthInfo["username"] + authPassword = instance.AuthInfo["password"] + case "OAUTH": + authToken = instance.AuthInfo["token"] + } + } + + err := huh.NewForm( + huh.NewGroup( + huh.NewNote(). + Title("Provider (cannot be changed)"). + Description(instance.Vendor), + huh.NewInput(). + Title("Name"). + Value(&instance.Name). + Validate(func(str string) error { + if strings.TrimSpace(str) == "" { + return errors.New("name cannot be empty or only spaces") + } + return nil + }), + huh.NewInput(). + Title("Description"). + Value(&instance.Description), + ), + huh.NewGroup( + huh.NewInput(). + Title("Endpoint"). + Value(&instance.Endpoint). + Validate(func(str string) error { + if strings.TrimSpace(str) == "" { + return errors.New("endpoint cannot be empty or only spaces") + } + formattedURL := utils.FormatUrl(str) + if err := utils.ValidateURL(formattedURL); err != nil { + return err + } + instance.Endpoint = formattedURL + return nil + }), + huh.NewConfirm(). + Title("Enable"). + Value(&instance.Enabled). + Affirmative("yes"). + Negative("no"). + WithButtonAlignment(lipgloss.Left), + huh.NewConfirm(). + Title("Skip Certificate Verification"). + Value(&instance.Insecure). + Affirmative("yes"). + Negative("no"). + WithButtonAlignment(lipgloss.Left), + ), + huh.NewGroup( + huh.NewSelect[string](). + Title("Auth Mode"). + Options( + huh.NewOption("None", "NONE"), + huh.NewOption("Basic", "BASIC"), + huh.NewOption("OAuth", "OAUTH"), + ). + Value(&instance.AuthMode), + ), + huh.NewGroup( + huh.NewInput(). + Title("Username"). + Value(&authUsername). + Validate(func(str string) error { + if instance.AuthMode != "BASIC" { + return nil + } + if strings.TrimSpace(str) == "" { + return errors.New("username cannot be empty or only spaces") + } + return nil + }), + huh.NewInput(). + Title("Password"). + EchoMode(huh.EchoModePassword). + Value(&authPassword). + Validate(func(str string) error { + if instance.AuthMode != "BASIC" { + return nil + } + if strings.TrimSpace(str) == "" { + return errors.New("password cannot be empty or only spaces") + } + return nil + }), + ).WithHideFunc(func() bool { + return instance.AuthMode == "NONE" || instance.AuthMode == "OAUTH" + }), + huh.NewGroup( + huh.NewInput(). + Title("Token"). + Value(&authToken). + Validate(func(str string) error { + if instance.AuthMode != "OAUTH" { + return nil + } + if strings.TrimSpace(str) == "" { + return errors.New("token cannot be empty or only spaces") + } + return nil + }), + ).WithHideFunc(func() bool { + return instance.AuthMode == "NONE" || instance.AuthMode == "BASIC" + }), + ).WithTheme(theme).Run() + if err != nil { + return err + } + + instance.AuthMode = strings.ToUpper(strings.TrimSpace(instance.AuthMode)) + + switch instance.AuthMode { + case "NONE": + instance.AuthInfo = nil + case "BASIC": + instance.AuthInfo = map[string]string{ + "username": strings.TrimSpace(authUsername), + "password": authPassword, + } + case "OAUTH": + instance.AuthInfo = map[string]string{ + "token": strings.TrimSpace(authToken), + } + } + + return nil +} From 915fffc702dd2b39a921963922cd9ba2d5142bb7 Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Thu, 16 Apr 2026 01:32:26 +0530 Subject: [PATCH 5/7] doc(instance): add update subcommand docs Signed-off-by: Sypher845 --- doc/cli-docs/harbor-instance-update.md | 55 ++++++++++++++ doc/cli-docs/harbor-instance.md | 1 + doc/man-docs/man1/harbor-instance-update.1 | 84 ++++++++++++++++++++++ doc/man-docs/man1/harbor-instance.1 | 2 +- 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 doc/cli-docs/harbor-instance-update.md create mode 100644 doc/man-docs/man1/harbor-instance-update.1 diff --git a/doc/cli-docs/harbor-instance-update.md b/doc/cli-docs/harbor-instance-update.md new file mode 100644 index 000000000..67dbfca61 --- /dev/null +++ b/doc/cli-docs/harbor-instance-update.md @@ -0,0 +1,55 @@ +--- +title: harbor instance update +weight: 20 +--- +## harbor instance update + +### Description + +##### Update a preheat provider instance in Harbor + +### Synopsis + +Update a preheat provider instance in Harbor by name or ID. If no update +flags are provided, the command opens an interactive update form. + +```sh +harbor instance update [NAME|ID] [flags] +``` + +### Examples + +```sh + harbor-cli instance update my-instance --description "Updated preheat instance" + harbor-cli instance update 1 --id --enable=false + harbor-cli instance update my-instance --authmode BASIC --auth-username admin --auth-password Harbor12345 +``` + +### Options + +```sh + --auth-password string Password for BASIC authentication + --auth-token string Token for OAUTH authentication + --auth-username string Username for BASIC authentication + -a, --authmode string Authentication mode (NONE, BASIC, OAUTH) + -d, --description string Description of the instance + --enable Whether the instance is enabled or not + -h, --help help for update + --id Get instance by id + --insecure Whether or not the certificate will be verified when Harbor tries to access the server + -n, --name string New name for the instance + -u, --url string Endpoint URL for the instance +``` + +### 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 instance](harbor-instance.md) - Manage preheat provider instances in Harbor + diff --git a/doc/cli-docs/harbor-instance.md b/doc/cli-docs/harbor-instance.md index 8b3f7dbda..ac15574b3 100644 --- a/doc/cli-docs/harbor-instance.md +++ b/doc/cli-docs/harbor-instance.md @@ -33,5 +33,6 @@ These instances represent external services such as Dragonfly or Kraken that hel * [harbor instance create](harbor-instance-create.md) - Create a new preheat provider instance in Harbor * [harbor instance delete](harbor-instance-delete.md) - Delete a preheat provider instance by its name or ID * [harbor instance list](harbor-instance-list.md) - List all preheat provider instances in Harbor +* [harbor instance update](harbor-instance-update.md) - Update a preheat provider instance in Harbor * [harbor instance view](harbor-instance-view.md) - get preheat provider instance by name or id diff --git a/doc/man-docs/man1/harbor-instance-update.1 b/doc/man-docs/man1/harbor-instance-update.1 new file mode 100644 index 000000000..3a69a803b --- /dev/null +++ b/doc/man-docs/man1/harbor-instance-update.1 @@ -0,0 +1,84 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-instance-update - Update a preheat provider instance in Harbor + + +.SH SYNOPSIS +\fBharbor instance update [NAME|ID] [flags]\fP + + +.SH DESCRIPTION +Update a preheat provider instance in Harbor by name or ID. If no update +flags are provided, the command opens an interactive update form. + + +.SH OPTIONS +\fB--auth-password\fP="" + Password for BASIC authentication + +.PP +\fB--auth-token\fP="" + Token for OAUTH authentication + +.PP +\fB--auth-username\fP="" + Username for BASIC authentication + +.PP +\fB-a\fP, \fB--authmode\fP="" + Authentication mode (NONE, BASIC, OAUTH) + +.PP +\fB-d\fP, \fB--description\fP="" + Description of the instance + +.PP +\fB--enable\fP[=false] + Whether the instance is enabled or not + +.PP +\fB-h\fP, \fB--help\fP[=false] + help for update + +.PP +\fB--id\fP[=false] + Get instance by id + +.PP +\fB--insecure\fP[=false] + Whether or not the certificate will be verified when Harbor tries to access the server + +.PP +\fB-n\fP, \fB--name\fP="" + New name for the instance + +.PP +\fB-u\fP, \fB--url\fP="" + Endpoint URL for the instance + + +.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-cli instance update my-instance --description "Updated preheat instance" + harbor-cli instance update 1 --id --enable=false + harbor-cli instance update my-instance --authmode BASIC --auth-username admin --auth-password Harbor12345 +.EE + + +.SH SEE ALSO +\fBharbor-instance(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-instance.1 b/doc/man-docs/man1/harbor-instance.1 index e6a54840b..1db9e5910 100644 --- a/doc/man-docs/man1/harbor-instance.1 +++ b/doc/man-docs/man1/harbor-instance.1 @@ -33,4 +33,4 @@ These instances represent external services such as Dragonfly or Kraken that hel .SH SEE ALSO -\fBharbor(1)\fP, \fBharbor-instance-create(1)\fP, \fBharbor-instance-delete(1)\fP, \fBharbor-instance-list(1)\fP, \fBharbor-instance-view(1)\fP \ No newline at end of file +\fBharbor(1)\fP, \fBharbor-instance-create(1)\fP, \fBharbor-instance-delete(1)\fP, \fBharbor-instance-list(1)\fP, \fBharbor-instance-update(1)\fP, \fBharbor-instance-view(1)\fP \ No newline at end of file From 4294137ff6e19d85e334af85f4d6896445fcc3bc Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Thu, 16 Apr 2026 02:39:14 +0530 Subject: [PATCH 6/7] feat(instance): add ping subcommand for preheat provider instances Signed-off-by: Sypher845 --- cmd/harbor/root/instance/cmd.go | 1 + cmd/harbor/root/instance/ping.go | 78 ++++++++++++++++++++++++++++++++ pkg/api/instance_handler.go | 24 ++++++++++ 3 files changed, 103 insertions(+) create mode 100644 cmd/harbor/root/instance/ping.go diff --git a/cmd/harbor/root/instance/cmd.go b/cmd/harbor/root/instance/cmd.go index ebebbd7b3..e8d166e3e 100644 --- a/cmd/harbor/root/instance/cmd.go +++ b/cmd/harbor/root/instance/cmd.go @@ -26,6 +26,7 @@ These instances represent external services such as Dragonfly or Kraken that hel CreateInstanceCommand(), DeleteInstanceCommand(), ListInstanceCommand(), + PingInstanceCommand(), UpdateInstanceCommand(), ViewInstanceCommand(), ) diff --git a/cmd/harbor/root/instance/ping.go b/cmd/harbor/root/instance/ping.go new file mode 100644 index 000000000..79643ce4d --- /dev/null +++ b/cmd/harbor/root/instance/ping.go @@ -0,0 +1,78 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package instance + +import ( + "fmt" + + "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" + "github.com/spf13/viper" +) + +func PingInstanceCommand() *cobra.Command { + var useInstanceID bool + + cmd := &cobra.Command{ + Use: "ping [NAME|ID]", + Short: "Ping preheat provider instance by name or id", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + var instanceName string + + if useInstanceID && len(args) == 0 { + return fmt.Errorf("instance ID must be provided when using --id") + } + + if len(args) > 0 { + log.Debugf("Instance name provided: %s", args[0]) + instanceName = args[0] + } else { + log.Debug("No instance name provided, prompting user") + instanceName, err = prompt.GetInstanceNameFromUser() + if err != nil { + return fmt.Errorf("failed to get instance name: %v", utils.ParseHarborErrorMsg(err)) + } + } + + log.Debugf("Pinging instance: %s", instanceName) + response, err := api.PingInstance(instanceName, useInstanceID) + if err != nil { + if utils.ParseHarborErrorCode(err) == "404" { + return fmt.Errorf("instance %s does not exist", instanceName) + } + return fmt.Errorf("failed to ping instance: %v", utils.ParseHarborErrorMsg(err)) + } + + outputFormat := viper.GetString("output-format") + if outputFormat != "" { + if err := utils.PrintFormat(response, outputFormat); err != nil { + return err + } + } else { + fmt.Printf("Instance '%s' pinged successfully\n", instanceName) + } + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&useInstanceID, "id", false, "Get instance by id") + + return cmd +} diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index 24dd8ed55..d946bceb2 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -76,6 +76,30 @@ func UpdateInstance(instanceName string, instance models.Instance) error { return nil } +func PingInstance(instanceNameOrID string, useInstanceID bool) (*preheat.PingInstancesOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + + instance, err := GetInstance(instanceNameOrID, useInstanceID) + if err != nil { + return nil, err + } + if instance == nil || instance.Payload == nil { + return nil, fmt.Errorf("failed to ping instance: empty response") + } + + response, err := client.Preheat.PingInstances(ctx, &preheat.PingInstancesParams{ + Instance: instance.Payload, + }) + if err != nil { + return nil, err + } + + return response, nil +} + func ListAllInstance(opts ...ListFlags) (*preheat.ListInstancesOK, error) { ctx, client, err := utils.ContextWithClient() if err != nil { From a9b9d73859b89fbb4c513b822b820625dcd775fa Mon Sep 17 00:00:00 2001 From: Sypher845 Date: Thu, 16 Apr 2026 02:41:51 +0530 Subject: [PATCH 7/7] doc(instance): add ping subcommand docs Signed-off-by: Sypher845 --- cmd/harbor/root/instance/update.go | 1 + doc/cli-docs/harbor-instance-ping.md | 33 ++++++++++++++++++++ doc/cli-docs/harbor-instance.md | 1 + doc/man-docs/man1/harbor-instance-ping.1 | 39 ++++++++++++++++++++++++ doc/man-docs/man1/harbor-instance.1 | 2 +- 5 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 doc/cli-docs/harbor-instance-ping.md create mode 100644 doc/man-docs/man1/harbor-instance-ping.1 diff --git a/cmd/harbor/root/instance/update.go b/cmd/harbor/root/instance/update.go index b7b20b96a..938e1fd03 100644 --- a/cmd/harbor/root/instance/update.go +++ b/cmd/harbor/root/instance/update.go @@ -1,6 +1,7 @@ // 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 diff --git a/doc/cli-docs/harbor-instance-ping.md b/doc/cli-docs/harbor-instance-ping.md new file mode 100644 index 000000000..0fbc8bb55 --- /dev/null +++ b/doc/cli-docs/harbor-instance-ping.md @@ -0,0 +1,33 @@ +--- +title: harbor instance ping +weight: 25 +--- +## harbor instance ping + +### Description + +##### Ping preheat provider instance by name or id + +```sh +harbor instance ping [NAME|ID] [flags] +``` + +### Options + +```sh + -h, --help help for ping + --id Get instance by id +``` + +### 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 instance](harbor-instance.md) - Manage preheat provider instances in Harbor + diff --git a/doc/cli-docs/harbor-instance.md b/doc/cli-docs/harbor-instance.md index ac15574b3..7409c3866 100644 --- a/doc/cli-docs/harbor-instance.md +++ b/doc/cli-docs/harbor-instance.md @@ -33,6 +33,7 @@ These instances represent external services such as Dragonfly or Kraken that hel * [harbor instance create](harbor-instance-create.md) - Create a new preheat provider instance in Harbor * [harbor instance delete](harbor-instance-delete.md) - Delete a preheat provider instance by its name or ID * [harbor instance list](harbor-instance-list.md) - List all preheat provider instances in Harbor +* [harbor instance ping](harbor-instance-ping.md) - Ping preheat provider instance by name or id * [harbor instance update](harbor-instance-update.md) - Update a preheat provider instance in Harbor * [harbor instance view](harbor-instance-view.md) - get preheat provider instance by name or id diff --git a/doc/man-docs/man1/harbor-instance-ping.1 b/doc/man-docs/man1/harbor-instance-ping.1 new file mode 100644 index 000000000..29f4c7aba --- /dev/null +++ b/doc/man-docs/man1/harbor-instance-ping.1 @@ -0,0 +1,39 @@ +.nh +.TH "HARBOR" "1" "Harbor Community" "Harbor User Manuals" + +.SH NAME +harbor-instance-ping - Ping preheat provider instance by name or id + + +.SH SYNOPSIS +\fBharbor instance ping [NAME|ID] [flags]\fP + + +.SH DESCRIPTION +Ping preheat provider instance by name or id + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for ping + +.PP +\fB--id\fP[=false] + Get instance by id + + +.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-instance(1)\fP \ No newline at end of file diff --git a/doc/man-docs/man1/harbor-instance.1 b/doc/man-docs/man1/harbor-instance.1 index 1db9e5910..2497ea85b 100644 --- a/doc/man-docs/man1/harbor-instance.1 +++ b/doc/man-docs/man1/harbor-instance.1 @@ -33,4 +33,4 @@ These instances represent external services such as Dragonfly or Kraken that hel .SH SEE ALSO -\fBharbor(1)\fP, \fBharbor-instance-create(1)\fP, \fBharbor-instance-delete(1)\fP, \fBharbor-instance-list(1)\fP, \fBharbor-instance-update(1)\fP, \fBharbor-instance-view(1)\fP \ No newline at end of file +\fBharbor(1)\fP, \fBharbor-instance-create(1)\fP, \fBharbor-instance-delete(1)\fP, \fBharbor-instance-list(1)\fP, \fBharbor-instance-ping(1)\fP, \fBharbor-instance-update(1)\fP, \fBharbor-instance-view(1)\fP \ No newline at end of file