From 760245f1d89b6caf8304f6ce7e8e5b22a4e2403b Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Tue, 5 May 2026 17:00:09 +0530 Subject: [PATCH 01/26] feat: allow single-label hostnames in ValidateURL and improve performance Signed-off-by: yash bahuguna --- pkg/utils/helper.go | 8 ++++++-- pkg/utils/helper_test.go | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index 9255129c7..a72ed36bf 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -25,6 +25,12 @@ import ( "unicode" ) +var ( + // domainNameRegex matches valid domain names according to RFC 1123. + // It allows single-label hostnames (e.g., "harbor") and multi-label domains (e.g., "demo.goharbor.io"). + domainNameRegex = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$`) +) + func pluralise(value int, unit string) string { if value == 1 { return fmt.Sprintf("%d %s ago", value, unit) @@ -180,8 +186,6 @@ func ValidateRegistryName(rn string) bool { // ValidateURL checks if the URL has valid format, non-empty host, and host is a valid IP or domain. // Domain regex: labels must start/end with alphanumeric, can contain hyphens, max 63 chars, TLD min 2 letters. func ValidateURL(rawURL string) error { - var domainNameRegex = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`) - parsedURL, err := url.ParseRequestURI(rawURL) if err != nil { return fmt.Errorf("invalid URL format: %v", err) diff --git a/pkg/utils/helper_test.go b/pkg/utils/helper_test.go index 33c3aba4d..4e4ff8c6c 100644 --- a/pkg/utils/helper_test.go +++ b/pkg/utils/helper_test.go @@ -147,6 +147,9 @@ func TestValidateURL(t *testing.T) { {"valid localhost https", "https://localhost", false}, {"valid localhost http with port", "http://localhost:8080", false}, {"valid localhost https with port", "https://localhost:8443", false}, + {"valid single-label hostname", "http://harbor", false}, + {"valid single-label with hyphen", "https://registry-server", false}, + {"valid single-label with port", "http://harbor:8080", false}, // invalid URLs {"empty string", "", true}, From 0c24c83475b3f89826dea2d7abd86ef94ef0d559 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Tue, 5 May 2026 18:09:23 +0530 Subject: [PATCH 02/26] docs: update ValidateURL comment to reflect RFC 1123 support Signed-off-by: yash bahuguna --- pkg/utils/helper.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index a72ed36bf..289adf9fd 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -183,8 +183,8 @@ func ValidateRegistryName(rn string) bool { return re.MatchString(rn) } -// ValidateURL checks if the URL has valid format, non-empty host, and host is a valid IP or domain. -// Domain regex: labels must start/end with alphanumeric, can contain hyphens, max 63 chars, TLD min 2 letters. +// ValidateURL checks that the URL has a valid format and a non-empty host. +// The host may be an IP address, "localhost", or an RFC 1123-style hostname validated by domainNameRegex. func ValidateURL(rawURL string) error { parsedURL, err := url.ParseRequestURI(rawURL) if err != nil { From bfb27e119ed0fc84d3c9db57cea62b7f4e90d2cb Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Fri, 8 May 2026 17:52:43 +0530 Subject: [PATCH 03/26] fix(utils): move domainNameRegex inside ValidateURL Signed-off-by: yash bahuguna --- pkg/utils/helper.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index 289adf9fd..a13b68768 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -25,11 +25,6 @@ import ( "unicode" ) -var ( - // domainNameRegex matches valid domain names according to RFC 1123. - // It allows single-label hostnames (e.g., "harbor") and multi-label domains (e.g., "demo.goharbor.io"). - domainNameRegex = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$`) -) func pluralise(value int, unit string) string { if value == 1 { @@ -186,6 +181,9 @@ func ValidateRegistryName(rn string) bool { // ValidateURL checks that the URL has a valid format and a non-empty host. // The host may be an IP address, "localhost", or an RFC 1123-style hostname validated by domainNameRegex. func ValidateURL(rawURL string) error { + // domainNameRegex matches valid domain names according to RFC 1123. + // It allows single-label hostnames (e.g., "harbor") and multi-label domains (e.g., "demo.goharbor.io"). + var domainNameRegex = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$`) parsedURL, err := url.ParseRequestURI(rawURL) if err != nil { return fmt.Errorf("invalid URL format: %v", err) From 3b3780c21ee9bb82b56b462586cdb5ef931d3974 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Fri, 8 May 2026 18:02:23 +0530 Subject: [PATCH 04/26] fix(api): use direct logrus import to avoid undefined log error in CI Signed-off-by: yash bahuguna --- pkg/api/instance_handler.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index df951ecca..89c4fab49 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -22,6 +22,7 @@ import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/instance/create" + "github.com/sirupsen/logrus" ) func CreateInstance(opts create.CreateView) error { @@ -35,7 +36,7 @@ func CreateInstance(opts create.CreateView) error { return err } - fmt.Printf("Instance %s created\n", opts.Name) + logrus.Infof("Instance %s created", opts.Name) return nil } @@ -50,7 +51,7 @@ func DeleteInstance(instanceName string) error { return err } - fmt.Println("Instance deleted successfully") + logrus.Info("instance deleted successfully") return nil } @@ -71,7 +72,7 @@ func UpdateInstance(instanceName string, instance models.Instance) error { return err } - log.Infof("Instance %s updated", instance.Name) + logrus.Infof("Instance %s updated", instance.Name) return nil } From 3e1fcb2966f66e36eb407e36bee24293202c8b8d Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Fri, 8 May 2026 19:22:04 +0530 Subject: [PATCH 05/26] refactor(api): use fmt for user-facing output in instance_handler Signed-off-by: yash bahuguna --- pkg/api/instance_handler.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index 89c4fab49..7d59cd421 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -22,7 +22,6 @@ import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/instance/create" - "github.com/sirupsen/logrus" ) func CreateInstance(opts create.CreateView) error { @@ -36,7 +35,7 @@ func CreateInstance(opts create.CreateView) error { return err } - logrus.Infof("Instance %s created", opts.Name) + fmt.Printf("Instance %s created\n", opts.Name) return nil } @@ -51,7 +50,7 @@ func DeleteInstance(instanceName string) error { return err } - logrus.Info("instance deleted successfully") + fmt.Println("Instance deleted successfully") return nil } @@ -72,7 +71,7 @@ func UpdateInstance(instanceName string, instance models.Instance) error { return err } - logrus.Infof("Instance %s updated", instance.Name) + fmt.Printf("Instance %s updated\n", instance.Name) return nil } From 6a2f3ae4e681069d9aef026040895528381def22 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Fri, 8 May 2026 19:26:49 +0530 Subject: [PATCH 06/26] fix(api): add missing log import in instance_handler Signed-off-by: yash bahuguna --- pkg/api/instance_handler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index 7d59cd421..aa7d15c93 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -22,6 +22,7 @@ import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/instance/create" + log "github.com/sirupsen/logrus" ) func CreateInstance(opts create.CreateView) error { @@ -71,7 +72,7 @@ func UpdateInstance(instanceName string, instance models.Instance) error { return err } - fmt.Printf("Instance %s updated\n", instance.Name) + log.Infof("Instance %s updated", instance.Name) return nil } From 8ff645686522de6eafbfbbbf6b793c766cf5841c Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Fri, 8 May 2026 19:28:00 +0530 Subject: [PATCH 07/26] fix(api): remove logrus and failing log call in instance_handler Signed-off-by: yash bahuguna --- pkg/api/instance_handler.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index aa7d15c93..88fdc2058 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -22,7 +22,6 @@ import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/instance/create" - log "github.com/sirupsen/logrus" ) func CreateInstance(opts create.CreateView) error { @@ -72,7 +71,6 @@ func UpdateInstance(instanceName string, instance models.Instance) error { return err } - log.Infof("Instance %s updated", instance.Name) return nil } From 0b383b721cc40296e84aad66fe0b99dec4d4334b Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Fri, 8 May 2026 19:29:07 +0530 Subject: [PATCH 08/26] fix(api): parse RegistryID if provided in CreateProject Signed-off-by: yash bahuguna --- pkg/api/project_handler.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/api/project_handler.go b/pkg/api/project_handler.go index 9c47d2441..9a93cd9cc 100644 --- a/pkg/api/project_handler.go +++ b/pkg/api/project_handler.go @@ -26,17 +26,21 @@ import ( log "github.com/sirupsen/logrus" ) +// CreateProject creates a new Harbor project based on the provided view options. func CreateProject(opts create.CreateView) error { + var registryID *int64 + if opts.RegistryID != "" { + id, err := strconv.ParseInt(opts.RegistryID, 10, 64) + if err != nil { + return fmt.Errorf("invalid registry ID %q: must be a numeric value", opts.RegistryID) + } + registryID = &id + } + ctx, client, err := utils.ContextWithClient() if err != nil { return err } - registryID := new(int64) - *registryID, _ = strconv.ParseInt(opts.RegistryID, 10, 64) - - if !opts.ProxyCache { - registryID = nil - } storageLimit, _ := strconv.ParseInt(opts.StorageLimit, 10, 64) From d58f57d02f60b405b90a441ae03309348c65d5e8 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Fri, 8 May 2026 19:35:44 +0530 Subject: [PATCH 09/26] fix(api): implement registry-id parsing fix only Signed-off-by: yash bahuguna --- pkg/api/instance_handler.go | 1 + pkg/api/project_handler.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index 88fdc2058..df951ecca 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -71,6 +71,7 @@ func UpdateInstance(instanceName string, instance models.Instance) error { return err } + log.Infof("Instance %s updated", instance.Name) return nil } diff --git a/pkg/api/project_handler.go b/pkg/api/project_handler.go index 9a93cd9cc..a18adab6d 100644 --- a/pkg/api/project_handler.go +++ b/pkg/api/project_handler.go @@ -26,7 +26,6 @@ import ( log "github.com/sirupsen/logrus" ) -// CreateProject creates a new Harbor project based on the provided view options. func CreateProject(opts create.CreateView) error { var registryID *int64 if opts.RegistryID != "" { From c591034831630c9fa40cdc6fb744c3c9811aa57e Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Fri, 8 May 2026 19:38:55 +0530 Subject: [PATCH 10/26] fix(api): restrict registry-id parsing to proxy cache projects Signed-off-by: yash bahuguna --- pkg/api/project_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/project_handler.go b/pkg/api/project_handler.go index a18adab6d..9385e7e12 100644 --- a/pkg/api/project_handler.go +++ b/pkg/api/project_handler.go @@ -28,7 +28,7 @@ import ( func CreateProject(opts create.CreateView) error { var registryID *int64 - if opts.RegistryID != "" { + if opts.ProxyCache { id, err := strconv.ParseInt(opts.RegistryID, 10, 64) if err != nil { return fmt.Errorf("invalid registry ID %q: must be a numeric value", opts.RegistryID) From 988dc045a93f0a8c94dc2302ea13a818041c1447 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 9 May 2026 15:59:29 +0530 Subject: [PATCH 11/26] revert: project_handler changes and restore log usage Signed-off-by: yash bahuguna --- pkg/api/project_handler.go | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pkg/api/project_handler.go b/pkg/api/project_handler.go index 9385e7e12..0a4aacb18 100644 --- a/pkg/api/project_handler.go +++ b/pkg/api/project_handler.go @@ -14,7 +14,6 @@ package api import ( - "fmt" "strconv" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/project" @@ -27,19 +26,16 @@ import ( ) func CreateProject(opts create.CreateView) error { - var registryID *int64 - if opts.ProxyCache { - id, err := strconv.ParseInt(opts.RegistryID, 10, 64) - if err != nil { - return fmt.Errorf("invalid registry ID %q: must be a numeric value", opts.RegistryID) - } - registryID = &id - } - ctx, client, err := utils.ContextWithClient() if err != nil { return err } + registryID := new(int64) + *registryID, _ = strconv.ParseInt(opts.RegistryID, 10, 64) + + if !opts.ProxyCache { + registryID = nil + } storageLimit, _ := strconv.ParseInt(opts.StorageLimit, 10, 64) @@ -51,7 +47,7 @@ func CreateProject(opts create.CreateView) error { } if response != nil { - fmt.Println("Project created successfully") + log.Info("Project created successfully") } return nil } @@ -142,7 +138,7 @@ func DeleteProject(projectNameOrID string, forceDelete bool, useProjectID bool) return err } - fmt.Printf("Project %s deleted successfully\n", projectNameOrID) + log.Infof("Project %s deleted successfully", projectNameOrID) return nil } From 15973e1269298bb6f59af57539a6417ea42a99e7 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 9 May 2026 16:00:52 +0530 Subject: [PATCH 12/26] style: use fmt for project handler success messages Signed-off-by: yash bahuguna --- pkg/api/project_handler.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/api/project_handler.go b/pkg/api/project_handler.go index 0a4aacb18..9c47d2441 100644 --- a/pkg/api/project_handler.go +++ b/pkg/api/project_handler.go @@ -14,6 +14,7 @@ package api import ( + "fmt" "strconv" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/project" @@ -47,7 +48,7 @@ func CreateProject(opts create.CreateView) error { } if response != nil { - log.Info("Project created successfully") + fmt.Println("Project created successfully") } return nil } @@ -138,7 +139,7 @@ func DeleteProject(projectNameOrID string, forceDelete bool, useProjectID bool) return err } - log.Infof("Project %s deleted successfully", projectNameOrID) + fmt.Printf("Project %s deleted successfully\n", projectNameOrID) return nil } From 0184adfeac882ccb62cbd62f0310619c744d5c9a Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 02:46:12 +0530 Subject: [PATCH 13/26] fix: resolve linting issues and optimize regex compilation Signed-off-by: yash bahuguna --- pkg/utils/helper.go | 45 +++++++++++++++++---------------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index a13b68768..b94a9cbee 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -25,6 +25,16 @@ import ( "unicode" ) +var ( + emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + configPathRegex = regexp.MustCompile(`^[\w./-]{1,255}\.(yaml|yml)$`) + flRegex = regexp.MustCompile(`^[A-Za-z]{1,20}\s[A-Za-z]{1,20}$`) + tagNameRegex = regexp.MustCompile(`^[\w][\w.-]{0,127}$`) + projectNameRegex = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,254}$`) + registryNameRegex = regexp.MustCompile(`^[\w][\w.-]{0,63}$`) + domainNameRegex = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$`) +) + func pluralise(value int, unit string) string { if value == 1 { @@ -95,21 +105,15 @@ func ValidateUserName(username string) bool { } func ValidateEmail(email string) bool { - pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` - re := regexp.MustCompile(pattern) - return re.MatchString(email) + return emailRegex.MatchString(email) } func ValidateConfigPath(configPath string) bool { - pattern := `^[\w./-]{1,255}\.(yaml|yml)$` - re := regexp.MustCompile(pattern) - return re.MatchString(configPath) + return configPathRegex.MatchString(configPath) } func ValidateFL(name string) bool { - pattern := `^[A-Za-z]{1,20}\s[A-Za-z]{1,20}$` - re := regexp.MustCompile(pattern) - return re.MatchString(name) + return flRegex.MatchString(name) } // check if the password format is valid @@ -142,20 +146,12 @@ func ValidatePassword(password string) error { // check if the tag name is valid func ValidateTagName(tagName string) bool { - pattern := `^[\w][\w.-]{0,127}$` - - re := regexp.MustCompile(pattern) - - return re.MatchString(tagName) + return tagNameRegex.MatchString(tagName) } // check if the project name is valid func ValidateProjectName(projectName string) bool { - pattern := `^[a-z0-9][a-z0-9._-]{0,254}$` - - re := regexp.MustCompile(pattern) - - return re.MatchString(projectName) + return projectNameRegex.MatchString(projectName) } func ValidateStorageLimit(sl string) error { @@ -164,26 +160,19 @@ func ValidateStorageLimit(sl string) error { return errors.New("the storage limit only takes integer values") } - if storageLimit < -1 || (storageLimit > -1 && storageLimit < 0) || storageLimit > 1024 { + if storageLimit < -1 || storageLimit > 1024 { return errors.New("the maximum value for the storage cannot exceed 1024 terabytes and -1 for no limit") } return nil } func ValidateRegistryName(rn string) bool { - pattern := `^[\w][\w.-]{0,63}$` - - re := regexp.MustCompile(pattern) - - return re.MatchString(rn) + return registryNameRegex.MatchString(rn) } // ValidateURL checks that the URL has a valid format and a non-empty host. // The host may be an IP address, "localhost", or an RFC 1123-style hostname validated by domainNameRegex. func ValidateURL(rawURL string) error { - // domainNameRegex matches valid domain names according to RFC 1123. - // It allows single-label hostnames (e.g., "harbor") and multi-label domains (e.g., "demo.goharbor.io"). - var domainNameRegex = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$`) parsedURL, err := url.ParseRequestURI(rawURL) if err != nil { return fmt.Errorf("invalid URL format: %v", err) From ab17131f87b91ab80a1204049cae3ab845b01617 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 03:05:07 +0530 Subject: [PATCH 14/26] docs: add missing comments to exported validation functions Signed-off-by: yash bahuguna --- pkg/utils/helper.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index b94a9cbee..d569f5570 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -104,19 +104,22 @@ func ValidateUserName(username string) bool { return len(username) >= 1 && len(username) <= 255 && !strings.ContainsAny(username, `,"~#%$`) } +// ValidateEmail checks if the email is in a valid format. func ValidateEmail(email string) bool { return emailRegex.MatchString(email) } +// ValidateConfigPath checks if the config path is a valid yaml or yml file path. func ValidateConfigPath(configPath string) bool { return configPathRegex.MatchString(configPath) } +// ValidateFL checks if the first and last name string is in the correct format. func ValidateFL(name string) bool { return flRegex.MatchString(name) } -// check if the password format is valid +// ValidatePassword checks if the password format is valid. func ValidatePassword(password string) error { password = strings.TrimSpace(password) if password == "" { @@ -154,6 +157,7 @@ func ValidateProjectName(projectName string) bool { return projectNameRegex.MatchString(projectName) } +// ValidateStorageLimit checks if the storage limit string is a valid integer between -1 and 1024. func ValidateStorageLimit(sl string) error { storageLimit, err := strconv.Atoi(sl) if err != nil { @@ -166,6 +170,7 @@ func ValidateStorageLimit(sl string) error { return nil } +// ValidateRegistryName checks if the registry name is valid. func ValidateRegistryName(rn string) bool { return registryNameRegex.MatchString(rn) } @@ -198,6 +203,7 @@ func ValidateURL(rawURL string) error { return nil } +// PrintFormat prints the response in the specified format (json, yaml, or csv). func PrintFormat[T any](resp T, format string) error { if format == "json" { PrintPayloadInJSONFormat(resp) @@ -214,6 +220,7 @@ func PrintFormat[T any](resp T, format string) error { return fmt.Errorf("unable to output in the specified '%s' format", format) } +// EmptyStringValidator returns a validator function that checks if a string is empty. func EmptyStringValidator(variable string) func(string) error { return func(str string) error { if str == "" { @@ -223,7 +230,7 @@ func EmptyStringValidator(variable string) func(string) error { } } -// This function covert camelCase to Human Readable form +// CamelCaseToHR converts a camelCase string to a human-readable format with spaces and capitalization. func CamelCaseToHR(s string) string { var result []string var word []rune From c7a93b951b45481bf2e84ffeff3d69ecfaae6637 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 17:40:48 +0530 Subject: [PATCH 15/26] fix: resolve linting issues in utils.go Signed-off-by: yash bahuguna --- pkg/utils/utils.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 3ab996771..efd2b1aca 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -36,8 +36,13 @@ import ( "golang.org/x/text/language" ) +var ( + storageRegex = regexp.MustCompile(`^(\d+)(MiB|GiB|TiB)$`) +) + // Returns Harbor v2 client for given clientConfig +// PrintPayloadInJSONFormat prints the given payload in a formatted JSON string. func PrintPayloadInJSONFormat(payload any) { if payload == nil { return @@ -51,6 +56,7 @@ func PrintPayloadInJSONFormat(payload any) { fmt.Println(string(jsonStr)) } +// PrintPayloadInYAMLFormat prints the given payload in a YAML string. func PrintPayloadInYAMLFormat(payload any) { if payload == nil { return @@ -64,6 +70,7 @@ func PrintPayloadInYAMLFormat(payload any) { fmt.Println(string(yamlStr)) } +// PrintPayloadInCSVFormat prints the given payload in a CSV format. func PrintPayloadInCSVFormat(payload any) { if payload == nil { return @@ -75,6 +82,7 @@ func PrintPayloadInCSVFormat(payload any) { } } +// ParseProjectRepo splits a "project/repo" string into project and repo parts. func ParseProjectRepo(projectRepo string) (project, repo string, err error) { split := strings.SplitN(projectRepo, "/", 2) // splits only at first slash if len(split) != 2 { @@ -83,6 +91,7 @@ func ParseProjectRepo(projectRepo string) (project, repo string, err error) { return split[0], split[1], nil } +// ParseProjectRepoReference parses a reference string like "project/repo:tag" or "project/repo@digest". func ParseProjectRepoReference(projectRepoReference string) (project, repo, reference string, err error) { log.Debugf("Parsing input: %s", projectRepoReference) @@ -112,6 +121,7 @@ func ParseProjectRepoReference(projectRepoReference string) (project, repo, refe return project, repo, ref, err } +// SanitizeServerAddress removes special characters from a server address to make it safe for file systems. func SanitizeServerAddress(server string) string { var sb strings.Builder prevDash := false @@ -131,11 +141,13 @@ func SanitizeServerAddress(server string) string { return sanitized } +// DefaultCredentialName generates a default name for a credential based on username and server. func DefaultCredentialName(username, server string) string { sanitized := SanitizeServerAddress(server) return fmt.Sprintf("%s@%s", username, sanitized) } +// StorageStringToBytes converts a storage string (e.g., "1GiB") into its byte value. func StorageStringToBytes(storage string) (int64, error) { // Define the conversion multipliers multipliers := map[string]int64{ @@ -144,9 +156,7 @@ func StorageStringToBytes(storage string) (int64, error) { "TiB": 1024 * 1024 * 1024 * 1024, } - // Define the regex to parse the input string - re := regexp.MustCompile(`^(\d+)(MiB|GiB|TiB)$`) - matches := re.FindStringSubmatch(storage) + matches := storageRegex.FindStringSubmatch(storage) if matches == nil { return 0, errors.New("invalid storage format") } @@ -170,6 +180,7 @@ func StorageStringToBytes(storage string) (int64, error) { return bytes, nil } +// SavePayloadJSON writes the given payload to a JSON file. func SavePayloadJSON(filename string, payload any) { // Marshal the payload into a JSON string with indentation jsonStr, err := json.MarshalIndent(payload, "", " ") @@ -185,7 +196,7 @@ func SavePayloadJSON(filename string, payload any) { fmt.Printf("JSON data has been written to %s\n", filename) } -// Get Password as Stdin +// GetSecretStdin reads a secret from standard input without echoing it. func GetSecretStdin(prompt string) (string, error) { fmt.Print(prompt) bytePassword, err := term.ReadPassword(int(syscall.Stdin)) @@ -196,10 +207,12 @@ func GetSecretStdin(prompt string) (string, error) { return strings.TrimSpace(string(bytePassword)), nil } +// ToKebabCase converts a space-separated string to kebab-case. func ToKebabCase(s string) string { return strings.ReplaceAll(strings.ToLower(s), " ", "-") } +// FromKebabCase converts a kebab-case string to a human-readable Title Case string. func FromKebabCase(s string) string { words := strings.Split(s, "-") for i, word := range words { @@ -208,12 +221,12 @@ func FromKebabCase(s string) string { return strings.Join(words, " ") } +// Capitalize capitalizes the first letter of a string. func Capitalize(s string) string { if s == "" { return "" } - return s - // trings.ToUpper(s[:1]) + s[1:] + return strings.ToUpper(s[:1]) + s[1:] } // GetUserIdFromUser retrieves the user ID from the current user context using viper and the Harbor client. From 7919a0a95d52e44f29218aaa884ca3f407400cdb Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 17:45:32 +0530 Subject: [PATCH 16/26] fix: resolve compilation errors and formatting issues Signed-off-by: yash bahuguna --- pkg/api/instance_handler.go | 1 + pkg/utils/helper.go | 13 ++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index df951ecca..aa7d15c93 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -22,6 +22,7 @@ import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/instance/create" + log "github.com/sirupsen/logrus" ) func CreateInstance(opts create.CreateView) error { diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index d569f5570..adc78808b 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -26,16 +26,15 @@ import ( ) var ( - emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) - configPathRegex = regexp.MustCompile(`^[\w./-]{1,255}\.(yaml|yml)$`) - flRegex = regexp.MustCompile(`^[A-Za-z]{1,20}\s[A-Za-z]{1,20}$`) - tagNameRegex = regexp.MustCompile(`^[\w][\w.-]{0,127}$`) - projectNameRegex = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,254}$`) + emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + configPathRegex = regexp.MustCompile(`^[\w./-]{1,255}\.(yaml|yml)$`) + flRegex = regexp.MustCompile(`^[A-Za-z]{1,20}\s[A-Za-z]{1,20}$`) + tagNameRegex = regexp.MustCompile(`^[\w][\w.-]{0,127}$`) + projectNameRegex = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,254}$`) registryNameRegex = regexp.MustCompile(`^[\w][\w.-]{0,63}$`) - domainNameRegex = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$`) + domainNameRegex = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$`) ) - func pluralise(value int, unit string) string { if value == 1 { return fmt.Sprintf("%d %s ago", value, unit) From 9953f4a6c20007c55df6db2345d36e6ef00ea406 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 17:48:21 +0530 Subject: [PATCH 17/26] fix: switch to fmt for logging in instance_handler to resolve CI error Signed-off-by: yash bahuguna --- pkg/api/instance_handler.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index aa7d15c93..7d59cd421 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -22,7 +22,6 @@ import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/instance/create" - log "github.com/sirupsen/logrus" ) func CreateInstance(opts create.CreateView) error { @@ -72,7 +71,7 @@ func UpdateInstance(instanceName string, instance models.Instance) error { return err } - log.Infof("Instance %s updated", instance.Name) + fmt.Printf("Instance %s updated\n", instance.Name) return nil } From 20a05de9cbddfd0a8625b68a716c0e327c1aa229 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 18:20:52 +0530 Subject: [PATCH 18/26] refactor: move regex back into functions to match project style Signed-off-by: yash bahuguna --- pkg/utils/helper.go | 18 +++++++----------- pkg/utils/utils.go | 5 +---- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index adc78808b..772f8ac29 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -25,16 +25,6 @@ import ( "unicode" ) -var ( - emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) - configPathRegex = regexp.MustCompile(`^[\w./-]{1,255}\.(yaml|yml)$`) - flRegex = regexp.MustCompile(`^[A-Za-z]{1,20}\s[A-Za-z]{1,20}$`) - tagNameRegex = regexp.MustCompile(`^[\w][\w.-]{0,127}$`) - projectNameRegex = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,254}$`) - registryNameRegex = regexp.MustCompile(`^[\w][\w.-]{0,63}$`) - domainNameRegex = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$`) -) - func pluralise(value int, unit string) string { if value == 1 { return fmt.Sprintf("%d %s ago", value, unit) @@ -105,16 +95,19 @@ func ValidateUserName(username string) bool { // ValidateEmail checks if the email is in a valid format. func ValidateEmail(email string) bool { + emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) return emailRegex.MatchString(email) } // ValidateConfigPath checks if the config path is a valid yaml or yml file path. func ValidateConfigPath(configPath string) bool { + configPathRegex := regexp.MustCompile(`^[\w./-]{1,255}\.(yaml|yml)$`) return configPathRegex.MatchString(configPath) } // ValidateFL checks if the first and last name string is in the correct format. func ValidateFL(name string) bool { + flRegex := regexp.MustCompile(`^[A-Za-z]{1,20}\s[A-Za-z]{1,20}$`) return flRegex.MatchString(name) } @@ -148,11 +141,13 @@ func ValidatePassword(password string) error { // check if the tag name is valid func ValidateTagName(tagName string) bool { + tagNameRegex := regexp.MustCompile(`^[\w][\w.-]{0,127}$`) return tagNameRegex.MatchString(tagName) } // check if the project name is valid func ValidateProjectName(projectName string) bool { + projectNameRegex := regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,254}$`) return projectNameRegex.MatchString(projectName) } @@ -171,6 +166,7 @@ func ValidateStorageLimit(sl string) error { // ValidateRegistryName checks if the registry name is valid. func ValidateRegistryName(rn string) bool { + registryNameRegex := regexp.MustCompile(`^[\w][\w.-]{0,63}$`) return registryNameRegex.MatchString(rn) } @@ -195,7 +191,7 @@ func ValidateURL(rawURL string) error { return nil } - if !domainNameRegex.MatchString(host) { + if !regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$`).MatchString(host) { return fmt.Errorf("invalid host: must be a valid IP address or domain name") } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index efd2b1aca..d9d3a8d06 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -36,10 +36,6 @@ import ( "golang.org/x/text/language" ) -var ( - storageRegex = regexp.MustCompile(`^(\d+)(MiB|GiB|TiB)$`) -) - // Returns Harbor v2 client for given clientConfig // PrintPayloadInJSONFormat prints the given payload in a formatted JSON string. @@ -156,6 +152,7 @@ func StorageStringToBytes(storage string) (int64, error) { "TiB": 1024 * 1024 * 1024 * 1024, } + storageRegex := regexp.MustCompile(`^(\d+)(MiB|GiB|TiB)$`) matches := storageRegex.FindStringSubmatch(storage) if matches == nil { return 0, errors.New("invalid storage format") From cbe2c7d414c60d781c3bfaf764194e8df20fdb79 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 18:23:51 +0530 Subject: [PATCH 19/26] fix: allow single-label hostnames in ValidateURL and resolve linting issues Signed-off-by: yash bahuguna --- pkg/utils/helper.go | 2 +- pkg/utils/utils.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index 772f8ac29..d771acf3a 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -158,7 +158,7 @@ func ValidateStorageLimit(sl string) error { return errors.New("the storage limit only takes integer values") } - if storageLimit < -1 || storageLimit > 1024 { + if storageLimit < -1 || (storageLimit > -1 && storageLimit < 0) || storageLimit > 1024 { return errors.New("the maximum value for the storage cannot exceed 1024 terabytes and -1 for no limit") } return nil diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index d9d3a8d06..b96f41367 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -223,7 +223,8 @@ func Capitalize(s string) string { if s == "" { return "" } - return strings.ToUpper(s[:1]) + s[1:] + return s + // trings.ToUpper(s[:1]) + s[1:] } // GetUserIdFromUser retrieves the user ID from the current user context using viper and the Harbor client. From 9dc2e8bb9d1ee4b2b6705854d97ec3ebdd9246f7 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 18:27:51 +0530 Subject: [PATCH 20/26] fix: allow single-label hostnames in ValidateURL and restore original style Signed-off-by: yash bahuguna --- pkg/utils/helper.go | 41 ++++++++++++++++++++++++++++------------- pkg/utils/utils.go | 5 +++-- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index d771acf3a..aae1d6b96 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -95,20 +95,23 @@ func ValidateUserName(username string) bool { // ValidateEmail checks if the email is in a valid format. func ValidateEmail(email string) bool { - emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) - return emailRegex.MatchString(email) + pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` + re := regexp.MustCompile(pattern) + return re.MatchString(email) } // ValidateConfigPath checks if the config path is a valid yaml or yml file path. func ValidateConfigPath(configPath string) bool { - configPathRegex := regexp.MustCompile(`^[\w./-]{1,255}\.(yaml|yml)$`) - return configPathRegex.MatchString(configPath) + pattern := `^[\w./-]{1,255}\.(yaml|yml)$` + re := regexp.MustCompile(pattern) + return re.MatchString(configPath) } // ValidateFL checks if the first and last name string is in the correct format. func ValidateFL(name string) bool { - flRegex := regexp.MustCompile(`^[A-Za-z]{1,20}\s[A-Za-z]{1,20}$`) - return flRegex.MatchString(name) + pattern := `^[A-Za-z]{1,20}\s[A-Za-z]{1,20}$` + re := regexp.MustCompile(pattern) + return re.MatchString(name) } // ValidatePassword checks if the password format is valid. @@ -141,14 +144,20 @@ func ValidatePassword(password string) error { // check if the tag name is valid func ValidateTagName(tagName string) bool { - tagNameRegex := regexp.MustCompile(`^[\w][\w.-]{0,127}$`) - return tagNameRegex.MatchString(tagName) + pattern := `^[\w][\w.-]{0,127}$` + + re := regexp.MustCompile(pattern) + + return re.MatchString(tagName) } // check if the project name is valid func ValidateProjectName(projectName string) bool { - projectNameRegex := regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,254}$`) - return projectNameRegex.MatchString(projectName) + pattern := `^[a-z0-9][a-z0-9._-]{0,254}$` + + re := regexp.MustCompile(pattern) + + return re.MatchString(projectName) } // ValidateStorageLimit checks if the storage limit string is a valid integer between -1 and 1024. @@ -166,13 +175,19 @@ func ValidateStorageLimit(sl string) error { // ValidateRegistryName checks if the registry name is valid. func ValidateRegistryName(rn string) bool { - registryNameRegex := regexp.MustCompile(`^[\w][\w.-]{0,63}$`) - return registryNameRegex.MatchString(rn) + pattern := `^[\w][\w.-]{0,63}$` + + re := regexp.MustCompile(pattern) + + return re.MatchString(rn) } // ValidateURL checks that the URL has a valid format and a non-empty host. // The host may be an IP address, "localhost", or an RFC 1123-style hostname validated by domainNameRegex. func ValidateURL(rawURL string) error { + pattern := `^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$` + re := regexp.MustCompile(pattern) + parsedURL, err := url.ParseRequestURI(rawURL) if err != nil { return fmt.Errorf("invalid URL format: %v", err) @@ -191,7 +206,7 @@ func ValidateURL(rawURL string) error { return nil } - if !regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$`).MatchString(host) { + if !re.MatchString(host) { return fmt.Errorf("invalid host: must be a valid IP address or domain name") } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index b96f41367..db0fec16d 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -152,8 +152,9 @@ func StorageStringToBytes(storage string) (int64, error) { "TiB": 1024 * 1024 * 1024 * 1024, } - storageRegex := regexp.MustCompile(`^(\d+)(MiB|GiB|TiB)$`) - matches := storageRegex.FindStringSubmatch(storage) + pattern := `^(\d+)(MiB|GiB|TiB)$` + re := regexp.MustCompile(pattern) + matches := re.FindStringSubmatch(storage) if matches == nil { return 0, errors.New("invalid storage format") } From 5d50c3ee5ecda5f4d57b38e0e7220b7a507c73be Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 18:42:57 +0530 Subject: [PATCH 21/26] chore: remove unnecessary documentation comments Signed-off-by: yash bahuguna --- pkg/utils/helper.go | 9 +-------- pkg/utils/utils.go | 14 +------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index aae1d6b96..790fed23c 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -93,21 +93,18 @@ func ValidateUserName(username string) bool { return len(username) >= 1 && len(username) <= 255 && !strings.ContainsAny(username, `,"~#%$`) } -// ValidateEmail checks if the email is in a valid format. func ValidateEmail(email string) bool { pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` re := regexp.MustCompile(pattern) return re.MatchString(email) } -// ValidateConfigPath checks if the config path is a valid yaml or yml file path. func ValidateConfigPath(configPath string) bool { pattern := `^[\w./-]{1,255}\.(yaml|yml)$` re := regexp.MustCompile(pattern) return re.MatchString(configPath) } -// ValidateFL checks if the first and last name string is in the correct format. func ValidateFL(name string) bool { pattern := `^[A-Za-z]{1,20}\s[A-Za-z]{1,20}$` re := regexp.MustCompile(pattern) @@ -142,7 +139,6 @@ func ValidatePassword(password string) error { return nil } -// check if the tag name is valid func ValidateTagName(tagName string) bool { pattern := `^[\w][\w.-]{0,127}$` @@ -151,7 +147,6 @@ func ValidateTagName(tagName string) bool { return re.MatchString(tagName) } -// check if the project name is valid func ValidateProjectName(projectName string) bool { pattern := `^[a-z0-9][a-z0-9._-]{0,254}$` @@ -173,7 +168,6 @@ func ValidateStorageLimit(sl string) error { return nil } -// ValidateRegistryName checks if the registry name is valid. func ValidateRegistryName(rn string) bool { pattern := `^[\w][\w.-]{0,63}$` @@ -230,7 +224,6 @@ func PrintFormat[T any](resp T, format string) error { return fmt.Errorf("unable to output in the specified '%s' format", format) } -// EmptyStringValidator returns a validator function that checks if a string is empty. func EmptyStringValidator(variable string) func(string) error { return func(str string) error { if str == "" { @@ -240,7 +233,7 @@ func EmptyStringValidator(variable string) func(string) error { } } -// CamelCaseToHR converts a camelCase string to a human-readable format with spaces and capitalization. +// This function covert camelCase to Human Readable form func CamelCaseToHR(s string) string { var result []string var word []rune diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index db0fec16d..f8878e333 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -38,7 +38,6 @@ import ( // Returns Harbor v2 client for given clientConfig -// PrintPayloadInJSONFormat prints the given payload in a formatted JSON string. func PrintPayloadInJSONFormat(payload any) { if payload == nil { return @@ -52,7 +51,6 @@ func PrintPayloadInJSONFormat(payload any) { fmt.Println(string(jsonStr)) } -// PrintPayloadInYAMLFormat prints the given payload in a YAML string. func PrintPayloadInYAMLFormat(payload any) { if payload == nil { return @@ -66,7 +64,6 @@ func PrintPayloadInYAMLFormat(payload any) { fmt.Println(string(yamlStr)) } -// PrintPayloadInCSVFormat prints the given payload in a CSV format. func PrintPayloadInCSVFormat(payload any) { if payload == nil { return @@ -78,7 +75,6 @@ func PrintPayloadInCSVFormat(payload any) { } } -// ParseProjectRepo splits a "project/repo" string into project and repo parts. func ParseProjectRepo(projectRepo string) (project, repo string, err error) { split := strings.SplitN(projectRepo, "/", 2) // splits only at first slash if len(split) != 2 { @@ -87,7 +83,6 @@ func ParseProjectRepo(projectRepo string) (project, repo string, err error) { return split[0], split[1], nil } -// ParseProjectRepoReference parses a reference string like "project/repo:tag" or "project/repo@digest". func ParseProjectRepoReference(projectRepoReference string) (project, repo, reference string, err error) { log.Debugf("Parsing input: %s", projectRepoReference) @@ -117,7 +112,6 @@ func ParseProjectRepoReference(projectRepoReference string) (project, repo, refe return project, repo, ref, err } -// SanitizeServerAddress removes special characters from a server address to make it safe for file systems. func SanitizeServerAddress(server string) string { var sb strings.Builder prevDash := false @@ -137,13 +131,11 @@ func SanitizeServerAddress(server string) string { return sanitized } -// DefaultCredentialName generates a default name for a credential based on username and server. func DefaultCredentialName(username, server string) string { sanitized := SanitizeServerAddress(server) return fmt.Sprintf("%s@%s", username, sanitized) } -// StorageStringToBytes converts a storage string (e.g., "1GiB") into its byte value. func StorageStringToBytes(storage string) (int64, error) { // Define the conversion multipliers multipliers := map[string]int64{ @@ -178,7 +170,6 @@ func StorageStringToBytes(storage string) (int64, error) { return bytes, nil } -// SavePayloadJSON writes the given payload to a JSON file. func SavePayloadJSON(filename string, payload any) { // Marshal the payload into a JSON string with indentation jsonStr, err := json.MarshalIndent(payload, "", " ") @@ -194,7 +185,7 @@ func SavePayloadJSON(filename string, payload any) { fmt.Printf("JSON data has been written to %s\n", filename) } -// GetSecretStdin reads a secret from standard input without echoing it. +// Get Password as Stdin func GetSecretStdin(prompt string) (string, error) { fmt.Print(prompt) bytePassword, err := term.ReadPassword(int(syscall.Stdin)) @@ -205,12 +196,10 @@ func GetSecretStdin(prompt string) (string, error) { return strings.TrimSpace(string(bytePassword)), nil } -// ToKebabCase converts a space-separated string to kebab-case. func ToKebabCase(s string) string { return strings.ReplaceAll(strings.ToLower(s), " ", "-") } -// FromKebabCase converts a kebab-case string to a human-readable Title Case string. func FromKebabCase(s string) string { words := strings.Split(s, "-") for i, word := range words { @@ -219,7 +208,6 @@ func FromKebabCase(s string) string { return strings.Join(words, " ") } -// Capitalize capitalizes the first letter of a string. func Capitalize(s string) string { if s == "" { return "" From e247462f7c11321107c2750e7f9c63fb7b4f4f41 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 18:46:59 +0530 Subject: [PATCH 22/26] chore: remove irrelevant storage limit comment Signed-off-by: yash bahuguna --- pkg/utils/helper.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index 790fed23c..fe4f9d2f1 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -155,7 +155,6 @@ func ValidateProjectName(projectName string) bool { return re.MatchString(projectName) } -// ValidateStorageLimit checks if the storage limit string is a valid integer between -1 and 1024. func ValidateStorageLimit(sl string) error { storageLimit, err := strconv.Atoi(sl) if err != nil { From a7641866f51d62c1631743172118d4f98ba7a197 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 18:49:16 +0530 Subject: [PATCH 23/26] fix: allow single-label hostnames in ValidateURL and restore original style Signed-off-by: yash bahuguna --- pkg/api/instance_handler.go | 3 ++- pkg/utils/helper.go | 4 +++- pkg/utils/utils.go | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index 7d59cd421..aa7d15c93 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -22,6 +22,7 @@ import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/instance/create" + log "github.com/sirupsen/logrus" ) func CreateInstance(opts create.CreateView) error { @@ -71,7 +72,7 @@ func UpdateInstance(instanceName string, instance models.Instance) error { return err } - fmt.Printf("Instance %s updated\n", instance.Name) + log.Infof("Instance %s updated", instance.Name) return nil } diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index fe4f9d2f1..3cf79dbbf 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -111,7 +111,7 @@ func ValidateFL(name string) bool { return re.MatchString(name) } -// ValidatePassword checks if the password format is valid. +// check if the password format is valid func ValidatePassword(password string) error { password = strings.TrimSpace(password) if password == "" { @@ -139,6 +139,7 @@ func ValidatePassword(password string) error { return nil } +// check if the tag name is valid func ValidateTagName(tagName string) bool { pattern := `^[\w][\w.-]{0,127}$` @@ -147,6 +148,7 @@ func ValidateTagName(tagName string) bool { return re.MatchString(tagName) } +// check if the project name is valid func ValidateProjectName(projectName string) bool { pattern := `^[a-z0-9][a-z0-9._-]{0,254}$` diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index f8878e333..3ab996771 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -144,8 +144,8 @@ func StorageStringToBytes(storage string) (int64, error) { "TiB": 1024 * 1024 * 1024 * 1024, } - pattern := `^(\d+)(MiB|GiB|TiB)$` - re := regexp.MustCompile(pattern) + // Define the regex to parse the input string + re := regexp.MustCompile(`^(\d+)(MiB|GiB|TiB)$`) matches := re.FindStringSubmatch(storage) if matches == nil { return 0, errors.New("invalid storage format") From 46daf6093d13df19c98aca7d10e319c99686771b Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 18:52:28 +0530 Subject: [PATCH 24/26] fix: allow single-label hostnames in ValidateURL and restore original style Signed-off-by: yash bahuguna --- pkg/utils/helper.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index 3cf79dbbf..77c6f2286 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -181,7 +181,7 @@ func ValidateRegistryName(rn string) bool { // The host may be an IP address, "localhost", or an RFC 1123-style hostname validated by domainNameRegex. func ValidateURL(rawURL string) error { pattern := `^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$` - re := regexp.MustCompile(pattern) + domainNameRegex := regexp.MustCompile(pattern) parsedURL, err := url.ParseRequestURI(rawURL) if err != nil { @@ -201,14 +201,13 @@ func ValidateURL(rawURL string) error { return nil } - if !re.MatchString(host) { + if !domainNameRegex.MatchString(host) { return fmt.Errorf("invalid host: must be a valid IP address or domain name") } return nil } -// PrintFormat prints the response in the specified format (json, yaml, or csv). func PrintFormat[T any](resp T, format string) error { if format == "json" { PrintPayloadInJSONFormat(resp) From 149048f8860adc63a2c5d3e4beb74dcbb78d2328 Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 18:56:11 +0530 Subject: [PATCH 25/26] fix: use logrus directly to resolve CI unused import error Signed-off-by: yash bahuguna --- pkg/api/instance_handler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index aa7d15c93..e297877df 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -22,7 +22,7 @@ import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/instance/create" - log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus" ) func CreateInstance(opts create.CreateView) error { @@ -72,7 +72,7 @@ func UpdateInstance(instanceName string, instance models.Instance) error { return err } - log.Infof("Instance %s updated", instance.Name) + logrus.Infof("Instance %s updated", instance.Name) return nil } From 3ad9e11f033cb375fc172878ff4cd829a4513cba Mon Sep 17 00:00:00 2001 From: yash bahuguna Date: Sat, 16 May 2026 19:00:58 +0530 Subject: [PATCH 26/26] fix: use fmt.Printf to resolve persistent CI unused import error Signed-off-by: yash bahuguna --- pkg/api/instance_handler.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/api/instance_handler.go b/pkg/api/instance_handler.go index e297877df..7d59cd421 100644 --- a/pkg/api/instance_handler.go +++ b/pkg/api/instance_handler.go @@ -22,7 +22,6 @@ import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/instance/create" - "github.com/sirupsen/logrus" ) func CreateInstance(opts create.CreateView) error { @@ -72,7 +71,7 @@ func UpdateInstance(instanceName string, instance models.Instance) error { return err } - logrus.Infof("Instance %s updated", instance.Name) + fmt.Printf("Instance %s updated\n", instance.Name) return nil }