From 3fe814a77a0d02980e420c7c3c72a89e19d59af8 Mon Sep 17 00:00:00 2001 From: Vadim Bauer Date: Fri, 1 Nov 2024 20:42:18 +0100 Subject: [PATCH 01/41] update readme (#233) Signed-off-by: Vadim Bauer Signed-off-by: Patrick Eschenbach --- README.md | 83 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index f16e0e8c..d01e8a57 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ **Welcome to the Harbor CLI project! This powerful command-line tool facilitates seamless interaction with the Harbor container registry. It simplifies various tasks such as creating, updating, and managing projects, registries, and other resources in Harbor.** +[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/harbor-cli)](https://artifacthub.io/packages/search?repo=harbor-cli) + # Project Scope 🧪 The Harbor CLI is designed to enhance your interaction with the Harbor container registry. Built on Golang, it offers a user-friendly interface to perform various tasks related to projects, registries, and more. Whether you're creating, updating, or managing resources, the Harbor CLI streamlines your workflow efficiently. @@ -15,10 +17,52 @@ The Harbor CLI is designed to enhance your interaction with the Harbor container 🔹 Run commands with various flags for enhanced functionality
🔹 More features coming soon... 🚧 -# Example Commands💡 +# Installation + +## Container + +It is straightforward to use the Harbor CLI as a container. You can run the following command to use the Harbor CLI as a container: + +```shell +docker run -ti --rm -v $HOME/.harbor/config.yaml:/root/.harbor/config.yaml registry.goharbor.io/harbor-cli/harbor-cli --help + +``` + +# Add the following command to create an alias and append the alias to your .zshrc or .bashrc file +```shell +echo "alias harbor='docker run -ti --rm -v \$HOME/.harbor/config.yaml:/root/.harbor/config.yaml registry.goharbor.io/harbor-cli/harbor-cli'" >> ~/.zshrc +source ~/.zshrc # or restart your terminal +``` + + +## Linux, MacOS and Windows + +Harbor CLI will soon be published on Homebrew. +Meantime, we recommend using Harbor in the Container +or download the binary from the [releases page](https://github.com/goharbor/harbor-cli/releases) + + + +## Add the Harbor CLI to your Container Image + +Using Curl or Wget isn't recommended +for adding the Harbor CLI to your container. +Instead, we recommend copying the Harbor CLI from our official image +by using the following Dockerfile: + +```Dockerfile +#... +COPY --from=registry.goharbor.io/harbor-cli/harbor-cli:latest /harbor /usr/local/bin/harbor +# --chown and --chmod flags can be used to set the permissions +``` + + + +# Example Commands💡 ```bash -âžœ harbor --help +>./harbor + Official Harbor CLI Usage: @@ -34,7 +78,9 @@ harbor help Available Commands: + artifact Manage artifacts completion Generate the autocompletion script for the specified shell + health Get the health status of Harbor components help Help about any command login Log in to Harbor registry project Manage projects and assign resources to them @@ -44,14 +90,20 @@ Available Commands: version Version of Harbor CLI Flags: - --config string config file (default is $HOME/.harbor/config.yaml) (default "/home/bishal/.harbor/config.yaml") + --config string config file (default is $HOME/.harbor/config.yaml) (default "/Users/vadim/.harbor/config.yaml") -h, --help help for harbor -o, --output-format string Output format. One of: json|yaml -v, --verbose verbose output Use "harbor [command] --help" for more information about a command. + + ``` + + + + #### Log in to Harbor Registry ```bash @@ -98,22 +150,6 @@ Linux | ✅ macOS | ✅ Windows | ✅ -# Installation - - -## Linux and MacOS - -Homebrew is the recommended way to install Harbor CLI on MacOS and Linux. - - -## Windows - -```shell - -winget install harbor - -``` - # Build From Source @@ -134,6 +170,15 @@ git clone https://github.com/goharbor/harbor-cli.git go build -o harbor-cli cmd/harbor/main.go ``` +# Version Compatibility With Harbor + +At the moment, the Harbor CLI is developed and tested with Harbor 2.11. +The CLI should work with versions prior to 2.11, +but not all functionalities may be available or work as expected. + +Harbor <2.0.0 is not supported. + + # Community From 08d7811fbaf8209e2593a4e78a62b6ac1534cb81 Mon Sep 17 00:00:00 2001 From: ALTHAF <114910365+Althaf66@users.noreply.github.com> Date: Sat, 2 Nov 2024 02:31:40 +0530 Subject: [PATCH 02/41] Fixed the registry info issue (#237) * fixed the issue#224 Signed-off-by: ALTHAF * fixed the issue#224 Signed-off-by: ALTHAF * some modification in registry Signed-off-by: ALTHAF --------- Signed-off-by: ALTHAF Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/registry/create.go | 8 ++++---- cmd/harbor/root/registry/delete.go | 14 ++++++-------- cmd/harbor/root/registry/info.go | 13 ++++++------- cmd/harbor/root/registry/update.go | 11 +++++------ pkg/api/registry_handler.go | 22 ++++++++++++++++++++++ 5 files changed, 43 insertions(+), 25 deletions(-) diff --git a/cmd/harbor/root/registry/create.go b/cmd/harbor/root/registry/create.go index 30efec3d..bf4d93ac 100644 --- a/cmd/harbor/root/registry/create.go +++ b/cmd/harbor/root/registry/create.go @@ -7,14 +7,14 @@ import ( "github.com/spf13/cobra" ) -// NewCreateRegistryCommand creates a new `harbor create registry` command func CreateRegistryCommand() *cobra.Command { var opts api.CreateRegView cmd := &cobra.Command{ - Use: "create", - Short: "create registry", - Args: cobra.NoArgs, + Use: "create", + Short: "create registry", + Example: "harbor registry create", + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { var err error createView := &api.CreateRegView{ diff --git a/cmd/harbor/root/registry/delete.go b/cmd/harbor/root/registry/delete.go index ed9d426b..92687744 100644 --- a/cmd/harbor/root/registry/delete.go +++ b/cmd/harbor/root/registry/delete.go @@ -1,27 +1,25 @@ package registry import ( - "strconv" - "github.com/goharbor/harbor-cli/pkg/api" "github.com/goharbor/harbor-cli/pkg/prompt" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -// NewDeleteRegistryCommand creates a new `harbor delete registry` command func DeleteRegistryCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "delete", - Short: "delete registry by id", - Args: cobra.MaximumNArgs(1), + Use: "delete", + Short: "delete registry by id", + Example: "harbor registry delete [registryname]", + Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error if len(args) > 0 { - registryId, _ := strconv.ParseInt(args[0], 10, 64) - err = api.DeleteRegistry(registryId) + registryName, _ := api.GetRegistryIdByName(args[0]) + err = api.DeleteRegistry(registryName) } else { registryId := prompt.GetRegistryNameFromUser() err = api.DeleteRegistry(registryId) diff --git a/cmd/harbor/root/registry/info.go b/cmd/harbor/root/registry/info.go index 17da984c..97a16799 100644 --- a/cmd/harbor/root/registry/info.go +++ b/cmd/harbor/root/registry/info.go @@ -1,8 +1,6 @@ package registry import ( - "strconv" - "github.com/goharbor/harbor-cli/pkg/api" "github.com/goharbor/harbor-cli/pkg/prompt" log "github.com/sirupsen/logrus" @@ -11,15 +9,16 @@ import ( func InfoRegistryCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "info", - Short: "get registry info", - Args: cobra.MaximumNArgs(1), + Use: "info", + Short: "get registry info", + Example: "harbor registry info [registryname]", + Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error if len(args) > 0 { - registryId, _ := strconv.ParseInt(args[0], 10, 64) - err = api.InfoRegistry(registryId) + registryName, _ := api.GetRegistryIdByName(args[0]) + err = api.InfoRegistry(registryName) } else { registryId := prompt.GetRegistryNameFromUser() err = api.InfoRegistry(registryId) diff --git a/cmd/harbor/root/registry/update.go b/cmd/harbor/root/registry/update.go index fde3cb4c..0db5db45 100644 --- a/cmd/harbor/root/registry/update.go +++ b/cmd/harbor/root/registry/update.go @@ -1,8 +1,6 @@ package registry import ( - "strconv" - "github.com/goharbor/harbor-cli/pkg/api" "github.com/goharbor/harbor-cli/pkg/prompt" "github.com/goharbor/harbor-cli/pkg/views/registry/create" @@ -15,9 +13,10 @@ func UpdateRegistryCommand() *cobra.Command { var opts api.CreateRegView cmd := &cobra.Command{ - Use: "update", - Short: "update registry", - Args: cobra.MaximumNArgs(1), + Use: "update", + Short: "update registry", + Example: "harbor registry update [registryname]", + Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error var registryId int64 @@ -36,7 +35,7 @@ func UpdateRegistryCommand() *cobra.Command { } if len(args) > 0 { - registryId, err = strconv.ParseInt(args[0], 10, 64) + registryId, err = api.GetRegistryIdByName(args[0]) } else { registryId = prompt.GetRegistryNameFromUser() } diff --git a/pkg/api/registry_handler.go b/pkg/api/registry_handler.go index aafcc147..e3a341cb 100644 --- a/pkg/api/registry_handler.go +++ b/pkg/api/registry_handler.go @@ -1,6 +1,8 @@ package api import ( + "fmt" + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/registry" "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" @@ -89,6 +91,9 @@ func InfoRegistry(registryId int64) error { if err != nil { return err } + if response.Payload.ID == 0 { + return fmt.Errorf("registry is not found") + } utils.PrintPayloadInJSONFormat(response.Payload) return nil @@ -138,3 +143,20 @@ func GetRegistryProviders() ([]string, error) { return response.Payload, nil } + +func GetRegistryIdByName(registryName string) (int64, error) { + var opts ListFlags + + r, err := ListRegistries(opts) + if err != nil { + return 0, err + } + + for _, registry := range r.Payload { + if registry.Name == registryName { + return registry.ID, nil + } + } + + return 0, err +} From b3ccfba3ef4a589c37861046c8985dc6a831545f Mon Sep 17 00:00:00 2001 From: Krishna Madhwani <100672872+Roaster05@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:18:32 +0530 Subject: [PATCH 03/41] fixed username validator (#239) Signed-off-by: Patrick Eschenbach --- pkg/utils/helper.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index 31f06c9b..05e149e2 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -44,10 +44,10 @@ func FormatSize(size int64) string { return fmt.Sprintf("%.2fMiB", mbSize) } +// ValidateUserName checks if the username is valid by length and allowed characters. func ValidateUserName(username string) bool { - pattern := `^[a-zA-Z0-9]{1,255}$` - re := regexp.MustCompile(pattern) - return re.MatchString(username) + username = strings.TrimSpace(username) + return len(username) >= 1 && len(username) <= 255 && !strings.ContainsAny(username, `,"~#%$`) } func ValidateEmail(email string) bool { From 3f0b3768cc4c2db72689bbca1ffa01cd1ab79428 Mon Sep 17 00:00:00 2001 From: Vadim Bauer Date: Tue, 12 Nov 2024 00:21:09 +0100 Subject: [PATCH 04/41] Revert "fixed username validator (#239)" (#247) This reverts commit d059ef2a8ff4422ec887502082dbf2618c76e1eb. Signed-off-by: Patrick Eschenbach --- pkg/utils/helper.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index 05e149e2..31f06c9b 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -44,10 +44,10 @@ func FormatSize(size int64) string { return fmt.Sprintf("%.2fMiB", mbSize) } -// ValidateUserName checks if the username is valid by length and allowed characters. func ValidateUserName(username string) bool { - username = strings.TrimSpace(username) - return len(username) >= 1 && len(username) <= 255 && !strings.ContainsAny(username, `,"~#%$`) + pattern := `^[a-zA-Z0-9]{1,255}$` + re := regexp.MustCompile(pattern) + return re.MatchString(username) } func ValidateEmail(email string) bool { From 26ea44633f9addeef62b01fd4aa96a2ecb0d781f Mon Sep 17 00:00:00 2001 From: Prasanth B <89722848+bupd@users.noreply.github.com> Date: Thu, 14 Nov 2024 23:40:03 +0530 Subject: [PATCH 05/41] AutoGenerate credential name in login (#250) * generate credential name Signed-off-by: bupd * feat: add support for the password-stdin flag in login flow Signed-off-by: karanngi * fix deps - fixes dependencies Signed-off-by: bupd * return stdout for tests Signed-off-by: bupd * update workflow Signed-off-by: bupd --------- Signed-off-by: bupd Signed-off-by: karanngi Co-authored-by: karanngi Signed-off-by: Patrick Eschenbach --- .github/workflows/default.yaml | 4 ++-- cmd/harbor/root/login.go | 41 +++++++++++++++++----------------- dagger.json | 2 +- dagger/main.go | 24 ++++++++++---------- go.mod | 1 + go.sum | 2 ++ pkg/utils/utils.go | 9 ++++++++ pkg/views/login/create.go | 17 +++++++++++--- 8 files changed, 62 insertions(+), 38 deletions(-) diff --git a/.github/workflows/default.yaml b/.github/workflows/default.yaml index 0e0a742c..59800436 100644 --- a/.github/workflows/default.yaml +++ b/.github/workflows/default.yaml @@ -49,14 +49,14 @@ jobs: with: version: ${{ steps.dagger_version.outputs.version }} verb: call - args: test + args: test export --path=./testResults - name: Build Binary uses: dagger/dagger-for-github@v6 with: version: ${{ steps.dagger_version.outputs.version }} verb: call - args: build-dev --platform linux/amd64 + args: build-dev --platform linux/amd64 export --path=./harbor-dev push-latest-images: if: github.event.pull_request == null && !startsWith(github.ref, 'refs/tags/v') diff --git a/cmd/harbor/root/login.go b/cmd/harbor/root/login.go index ec727c22..316ded87 100644 --- a/cmd/harbor/root/login.go +++ b/cmd/harbor/root/login.go @@ -3,13 +3,14 @@ package root import ( "context" "fmt" - "strings" + "os" "github.com/goharbor/go-client/pkg/harbor" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/user" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/login" "github.com/spf13/cobra" + "golang.org/x/term" ) var ( @@ -17,6 +18,7 @@ var ( Username string Password string Name string + passwordStdin bool ) // LoginCommand creates a new `harbor login` command @@ -31,6 +33,16 @@ func LoginCommand() *cobra.Command { serverAddress = args[0] } + if passwordStdin { + fmt.Print("Password: ") + passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return fmt.Errorf("failed to read password from stdin: %v", err) + } + fmt.Println() + Password = string(passwordBytes) + } + loginView := login.LoginView{ Server: serverAddress, Username: Username, @@ -38,10 +50,14 @@ func LoginCommand() *cobra.Command { Name: Name, } + // autogenerate name + if loginView.Name == "" && loginView.Server != "" && loginView.Username != "" { + loginView.Name = fmt.Sprintf("%s@%s", loginView.Username, utils.SanitizeServerAddress(loginView.Server)) + } + var err error - if loginView.Server != "" && loginView.Username != "" && loginView.Password != "" && - loginView.Name != "" { + if loginView.Server != "" && loginView.Username != "" && loginView.Password != "" { err = runLogin(loginView) } else { err = createLoginView(&loginView) @@ -58,24 +74,11 @@ func LoginCommand() *cobra.Command { flags.StringVarP(&Name, "name", "", "", "name for the set of credentials") flags.StringVarP(&Username, "username", "u", "", "Username") flags.StringVarP(&Password, "password", "p", "", "Password") + flags.BoolVar(&passwordStdin, "password-stdin", false, "Take the password from stdin") return cmd } -// generateCredentialName creates a default credential name based on server and username -func generateCredentialName(server, username string) string { - if strings.HasPrefix(server, "http://") { - server = strings.ReplaceAll(server, "http://", "") - } - if strings.HasPrefix(server, "https://") { - server = strings.ReplaceAll(server, "https://", "") - } - if username != "" { - return fmt.Sprintf("%s@%s", username, server) - } - return server -} - func createLoginView(loginView *login.LoginView) error { if loginView == nil { loginView = &login.LoginView{ @@ -86,6 +89,7 @@ func createLoginView(loginView *login.LoginView) error { } } login.CreateView(loginView) + return runLogin(*loginView) } @@ -104,9 +108,6 @@ func runLogin(opts login.LoginView) error { if err != nil { return fmt.Errorf("login failed, please check your credentials: %s", err) } - if opts.Name == "" { - opts.Name = generateCredentialName(opts.Server, opts.Username) - } cred := utils.Credential{ Name: opts.Name, diff --git a/dagger.json b/dagger.json index 87feeb60..fa623ae3 100644 --- a/dagger.json +++ b/dagger.json @@ -9,5 +9,5 @@ } ], "source": "dagger", - "engineVersion": "v0.13.6" + "engineVersion": "v0.14.0" } diff --git a/dagger/main.go b/dagger/main.go index a99871ca..762f1344 100644 --- a/dagger/main.go +++ b/dagger/main.go @@ -16,9 +16,9 @@ const ( ) func New( -// Local or remote directory with source code, defaults to "./" -// +optional -// +defaultPath="./" + // Local or remote directory with source code, defaults to "./" + // +optional + // +defaultPath="./" source *dagger.Directory, ) *HarborCli { return &HarborCli{Source: source} @@ -122,8 +122,8 @@ func (m *HarborCli) lint(ctx context.Context) *dagger.Container { func (m *HarborCli) PublishImage( ctx context.Context, registry, registryUsername string, -// +optional -// +default=["latest"] + // +optional + // +default=["latest"] imageTags []string, registryPassword *dagger.Secret) []string { builders := m.build(ctx) @@ -234,7 +234,7 @@ func (m *HarborCli) Test(ctx context.Context) *dagger.Directory { WithEnvVariable("GOCACHE", "/go/build-cache"). WithMountedDirectory("/src", m.Source). WithWorkdir("/src"). - WithExec([]string{"go", "test", "-v", "./..."}). + WithExec([]string{"go", "test", "./..."}). Directory("/src") } @@ -254,11 +254,11 @@ func (m *HarborCli) PublishImageAndSign( registryUsername string, registryPassword *dagger.Secret, imageTags []string, -// +optional + // +optional githubToken *dagger.Secret, -// +optional + // +optional actionsIdTokenRequestToken *dagger.Secret, -// +optional + // +optional actionsIdTokenRequestUrl string, ) (string, error) { @@ -282,11 +282,11 @@ func (m *HarborCli) PublishImageAndSign( // Sign signs a container image using Cosign, works also with GitHub Actions func (m *HarborCli) Sign(ctx context.Context, -// +optional + // +optional githubToken *dagger.Secret, -// +optional + // +optional actionsIdTokenRequestUrl string, -// +optional + // +optional actionsIdTokenRequestToken *dagger.Secret, registryUsername string, registryPassword *dagger.Secret, diff --git a/go.mod b/go.mod index 9e37e8ce..14b1fa00 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 + golang.org/x/term v0.6.0 ) require ( diff --git a/go.sum b/go.sum index a2802a44..a9cd7625 100644 --- a/go.sum +++ b/go.sum @@ -179,6 +179,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index a64a67c7..db99de76 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -3,6 +3,7 @@ package utils import ( "encoding/json" "fmt" + "regexp" "strings" log "github.com/sirupsen/logrus" @@ -38,3 +39,11 @@ func ParseProjectRepoReference(projectRepoReference string) (string, string, str } return split[0], split[1], split[2] } + +func SanitizeServerAddress(server string) string { + re := regexp.MustCompile(`^https?://`) + server = re.ReplaceAllString(server, "") + re = regexp.MustCompile(`[^a-zA-Z0-9]`) + server = re.ReplaceAllString(server, "-") + return server +} diff --git a/pkg/views/login/create.go b/pkg/views/login/create.go index b0b9a4e9..0838845d 100644 --- a/pkg/views/login/create.go +++ b/pkg/views/login/create.go @@ -2,6 +2,7 @@ package login import ( "errors" + "fmt" "net/url" "strings" @@ -19,10 +20,12 @@ type LoginView struct { func CreateView(loginView *LoginView) { theme := huh.ThemeCharm() + err := huh.NewForm( huh.NewGroup( huh.NewInput(). Title("Server"). + Description("Server address eg. demo.goharbor.io"). Value(&loginView.Server). Validate(func(str string) error { if strings.TrimSpace(str) == "" { @@ -62,17 +65,25 @@ func CreateView(loginView *LoginView) { huh.NewInput(). Title("Name of Credential"). Value(&loginView.Name). + Description("Name of credential to be stored in the harbor config file."). + PlaceholderFunc(func() string { + return fmt.Sprintf("%s@%s", loginView.Username, utils.SanitizeServerAddress(loginView.Server)) + }, &loginView). + SuggestionsFunc(func() []string { + return []string{ + fmt.Sprintf("%s@%s", loginView.Username, utils.SanitizeServerAddress(loginView.Server)), + } + }, &loginView). Validate(func(str string) error { if str == "" { - return errors.New("credential name cannot be empty") + loginView.Name = fmt.Sprintf("%s@%s", loginView.Username, utils.SanitizeServerAddress(loginView.Server)) + return nil } return nil }), ), ).WithTheme(theme).Run() - if err != nil { log.Fatal(err) } - } From 71bebe86f96988c3f8cce32217529a4e064e7872 Mon Sep 17 00:00:00 2001 From: Vadim Bauer Date: Thu, 14 Nov 2024 19:30:29 +0100 Subject: [PATCH 06/41] print test output to screen (#254) print test output to screen Signed-off-by: Patrick Eschenbach --- .github/actions/publish-and-sign/action.yaml | 2 +- .github/workflows/default.yaml | 10 +++++----- dagger/main.go | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/actions/publish-and-sign/action.yaml b/.github/actions/publish-and-sign/action.yaml index 6422743d..73a613fd 100644 --- a/.github/actions/publish-and-sign/action.yaml +++ b/.github/actions/publish-and-sign/action.yaml @@ -34,7 +34,7 @@ runs: run: cosign env - name: Publish and Sign Snapshot Image - uses: dagger/dagger-for-github@v6 + uses: dagger/dagger-for-github@v7 env: GITHUB_TOKEN: ${{ inputs.GITHUB_TOKEN }} REGISTRY_ADDRESS: ${{ inputs.REGISTRY_ADDRESS }} diff --git a/.github/workflows/default.yaml b/.github/workflows/default.yaml index 59800436..c1953237 100644 --- a/.github/workflows/default.yaml +++ b/.github/workflows/default.yaml @@ -23,7 +23,7 @@ jobs: uses: sagikazarmark/dagger-version-action@v0.0.1 - name: Run Dagger golangci-lint - uses: dagger/dagger-for-github@v6 + uses: dagger/dagger-for-github@v7 with: version: ${{ steps.dagger_version.outputs.version }} verb: call @@ -45,14 +45,14 @@ jobs: fetch-depth: 0 - name: Run Tests - uses: dagger/dagger-for-github@v6 + uses: dagger/dagger-for-github@v7 with: version: ${{ steps.dagger_version.outputs.version }} verb: call - args: test export --path=./testResults + args: test - name: Build Binary - uses: dagger/dagger-for-github@v6 + uses: dagger/dagger-for-github@v7 with: version: ${{ steps.dagger_version.outputs.version }} verb: call @@ -99,7 +99,7 @@ jobs: with: fetch-depth: 0 - name: Create Release - uses: dagger/dagger-for-github@v6 + uses: dagger/dagger-for-github@v7 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/dagger/main.go b/dagger/main.go index 762f1344..1035123f 100644 --- a/dagger/main.go +++ b/dagger/main.go @@ -225,8 +225,8 @@ func (m *HarborCli) RunDoc(ctx context.Context) *dagger.Directory { } // Test Executes Go tests and returns the directory containing the test results -func (m *HarborCli) Test(ctx context.Context) *dagger.Directory { - return dag.Container(). +func (m *HarborCli) Test(ctx context.Context) (string, error) { + test := dag.Container(). From("golang:"+GO_VERSION+"-alpine"). WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)). WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). @@ -234,8 +234,8 @@ func (m *HarborCli) Test(ctx context.Context) *dagger.Directory { WithEnvVariable("GOCACHE", "/go/build-cache"). WithMountedDirectory("/src", m.Source). WithWorkdir("/src"). - WithExec([]string{"go", "test", "./..."}). - Directory("/src") + WithExec([]string{"go", "test", "-v", "./..."}) + return test.Stdout(ctx) } // Parse the platform string into os and arch From 7af2b83f77a371e49ce1ea318d1692a9cc9e48a9 Mon Sep 17 00:00:00 2001 From: ALTHAF <114910365+Althaf66@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:05:12 +0530 Subject: [PATCH 07/41] modified project list cmd (#260) Signed-off-by: ALTHAF Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/project/list.go | 23 +++++++++++++++++++---- pkg/api/project_handler.go | 16 ++++++++++++++++ pkg/prompt/prompt.go | 2 +- pkg/views/project/list/view.go | 4 ++-- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/cmd/harbor/root/project/list.go b/cmd/harbor/root/project/list.go index e8a5594b..71f18de2 100644 --- a/cmd/harbor/root/project/list.go +++ b/cmd/harbor/root/project/list.go @@ -1,6 +1,7 @@ package project import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/project" "github.com/goharbor/harbor-cli/pkg/api" "github.com/goharbor/harbor-cli/pkg/utils" list "github.com/goharbor/harbor-cli/pkg/views/project/list" @@ -9,15 +10,28 @@ import ( "github.com/spf13/viper" ) -// NewListProjectCommand creates a new `harbor list project` command func ListProjectCommand() *cobra.Command { var opts api.ListFlags - + var private bool + var public bool + var projects project.ListProjectsOK + var err error cmd := &cobra.Command{ Use: "list", Short: "list project", Run: func(cmd *cobra.Command, args []string) { - projects, err := api.ListProject(opts) + if private && public { + log.Fatal("Cannot specify both --private and --public flags") + } else if private { + opts.Public = false + projects, err = api.ListProject(opts) + } else if public { + opts.Public = true + projects, err = api.ListProject(opts) + } else { + projects, err = api.ListAllProjects(opts) + } + if err != nil { log.Fatalf("failed to get projects list: %v", err) } @@ -35,7 +49,8 @@ func ListProjectCommand() *cobra.Command { flags.StringVarP(&opts.Name, "name", "", "", "Name of the project") flags.Int64VarP(&opts.Page, "page", "", 1, "Page number") flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page") - flags.BoolVarP(&opts.Public, "public", "", false, "Project is public or private") + flags.BoolVarP(&private, "private", "", false, "Show only private projects") + flags.BoolVarP(&public, "public", "", false, "Show only public projects") flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources") flags.StringVarP(&opts.Sort, "sort", "", "", "Sort the resource list in ascending or descending order") diff --git a/pkg/api/project_handler.go b/pkg/api/project_handler.go index 07824a30..3ab9f022 100644 --- a/pkg/api/project_handler.go +++ b/pkg/api/project_handler.go @@ -86,6 +86,22 @@ func ListProject(opts ...ListFlags) (project.ListProjectsOK, error) { return *response, nil } +func ListAllProjects(opts ...ListFlags) (project.ListProjectsOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return project.ListProjectsOK{}, err + } + var listFlags ListFlags + if len(opts) > 0 { + listFlags = opts[0] + } + response, err := client.Project.ListProjects(ctx, &project.ListProjectsParams{Page: &listFlags.Page, PageSize: &listFlags.PageSize, Q: &listFlags.Q, Sort: &listFlags.Sort, Name: &listFlags.Name}) + if err != nil { + return project.ListProjectsOK{}, err + } + return *response, nil +} + func LogsProject(projectName string) (*project.GetLogsOK, error) { ctx, client, err := utils.ContextWithClient() if err != nil { diff --git a/pkg/prompt/prompt.go b/pkg/prompt/prompt.go index f67f9b94..ccf96a5c 100644 --- a/pkg/prompt/prompt.go +++ b/pkg/prompt/prompt.go @@ -26,7 +26,7 @@ func GetRegistryNameFromUser() int64 { func GetProjectNameFromUser() string { projectName := make(chan string) go func() { - response, _ := api.ListProject() + response, _ := api.ListAllProjects() pview.ProjectList(response.Payload, projectName) }() diff --git a/pkg/views/project/list/view.go b/pkg/views/project/list/view.go index 5f979844..51dbb487 100644 --- a/pkg/views/project/list/view.go +++ b/pkg/views/project/list/view.go @@ -14,11 +14,11 @@ import ( var columns = []table.Column{ {Title: "ID", Width: 6}, - {Title: "Project Name", Width: 12}, + {Title: "Project Name", Width: 20}, {Title: "Access Level", Width: 12}, {Title: "Type", Width: 12}, {Title: "Repo Count", Width: 12}, - {Title: "Creation Time", Width: 30}, + {Title: "Creation Time", Width: 18}, } func ListProjects(projects []*models.Project) { From 523faba7aae098f306de326e782022bd88e2cc55 Mon Sep 17 00:00:00 2001 From: ALTHAF <114910365+Althaf66@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:25:54 +0530 Subject: [PATCH 08/41] Added flags for user list cmd (#238) * added flags for user list cmd Signed-off-by: ALTHAF * changed cli flows to the id to name Signed-off-by: ALTHAF * added flags for artifact list cmd Signed-off-by: ALTHAF --------- Signed-off-by: ALTHAF Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/artifact/list.go | 12 ++++++++++-- cmd/harbor/root/user/delete.go | 6 ++---- cmd/harbor/root/user/elevate.go | 4 +--- cmd/harbor/root/user/list.go | 10 +++++++++- pkg/api/artifact_handler.go | 10 +++++++++- pkg/api/user_handler.go | 31 ++++++++++++++++++++++++++++--- 6 files changed, 59 insertions(+), 14 deletions(-) diff --git a/cmd/harbor/root/artifact/list.go b/cmd/harbor/root/artifact/list.go index 22be9278..5c6fb33f 100644 --- a/cmd/harbor/root/artifact/list.go +++ b/cmd/harbor/root/artifact/list.go @@ -11,6 +11,8 @@ import ( ) func ListArtifactCommand() *cobra.Command { + var opts api.ListFlags + cmd := &cobra.Command{ Use: "list", Short: "list artifacts within a repository", @@ -21,11 +23,11 @@ func ListArtifactCommand() *cobra.Command { if len(args) > 0 { projectName, repoName := utils.ParseProjectRepo(args[0]) - resp, err = api.ListArtifact(projectName, repoName) + resp, err = api.ListArtifact(projectName, repoName, opts) } else { projectName := prompt.GetProjectNameFromUser() repoName := prompt.GetRepoNameFromUser(projectName) - resp, err = api.ListArtifact(projectName, repoName) + resp, err = api.ListArtifact(projectName, repoName, opts) } if err != nil { @@ -35,5 +37,11 @@ func ListArtifactCommand() *cobra.Command { }, } + flags := cmd.Flags() + flags.Int64VarP(&opts.Page, "page", "p", 1, "Page number") + flags.Int64VarP(&opts.PageSize, "page-size", "n", 10, "Size of per page") + flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources") + flags.StringVarP(&opts.Sort, "sort", "s", "", "Sort the resource list in ascending or descending order") + return cmd } diff --git a/cmd/harbor/root/user/delete.go b/cmd/harbor/root/user/delete.go index 1b5afc8a..43c24528 100644 --- a/cmd/harbor/root/user/delete.go +++ b/cmd/harbor/root/user/delete.go @@ -1,8 +1,6 @@ package user import ( - "strconv" - "github.com/goharbor/harbor-cli/pkg/api" "github.com/goharbor/harbor-cli/pkg/prompt" log "github.com/sirupsen/logrus" @@ -17,8 +15,8 @@ func UserDeleteCmd() *cobra.Command { Run: func(cmd *cobra.Command, args []string) { var err error if len(args) > 0 { - userId, _ := strconv.ParseInt(args[0], 10, 64) - err = api.DeleteUser(userId) + userName, _ := api.GetUsersIdByName(args[0]) + err = api.DeleteUser(userName) } else { userId := prompt.GetUserIdFromUser() diff --git a/cmd/harbor/root/user/elevate.go b/cmd/harbor/root/user/elevate.go index 25cc7299..66a38567 100644 --- a/cmd/harbor/root/user/elevate.go +++ b/cmd/harbor/root/user/elevate.go @@ -1,8 +1,6 @@ package user import ( - "strconv" - "github.com/goharbor/harbor-cli/pkg/api" "github.com/goharbor/harbor-cli/pkg/prompt" "github.com/goharbor/harbor-cli/pkg/views" @@ -20,7 +18,7 @@ func ElevateUserCmd() *cobra.Command { var err error var userId int64 if len(args) > 0 { - userId, _ = strconv.ParseInt(args[0], 10, 64) + userId, _ = api.GetUsersIdByName(args[0]) } else { userId = prompt.GetUserIdFromUser() diff --git a/cmd/harbor/root/user/list.go b/cmd/harbor/root/user/list.go index b9f356e9..d8a81aab 100644 --- a/cmd/harbor/root/user/list.go +++ b/cmd/harbor/root/user/list.go @@ -10,13 +10,15 @@ import ( ) func UserListCmd() *cobra.Command { + var opts api.ListFlags + cmd := &cobra.Command{ Use: "list", Short: "list users", Args: cobra.NoArgs, Aliases: []string{"ls"}, Run: func(cmd *cobra.Command, args []string) { - response, err := api.ListUsers() + response, err := api.ListUsers(opts) if err != nil { log.Errorf("failed to list users: %v", err) return @@ -30,6 +32,12 @@ func UserListCmd() *cobra.Command { }, } + flags := cmd.Flags() + flags.Int64VarP(&opts.Page, "page", "p", 1, "Page number") + flags.Int64VarP(&opts.PageSize, "page-size", "n", 10, "Size of per page") + flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources") + flags.StringVarP(&opts.Sort, "sort", "s", "", "Sort the resource list in ascending or descending order") + return cmd } diff --git a/pkg/api/artifact_handler.go b/pkg/api/artifact_handler.go index cdfc6764..a6fa1391 100644 --- a/pkg/api/artifact_handler.go +++ b/pkg/api/artifact_handler.go @@ -51,15 +51,23 @@ func InfoArtifact(projectName, repoName, reference string) error { } // RunListArtifact lists all artifacts in a repository. -func ListArtifact(projectName, repoName string) (artifact.ListArtifactsOK, error) { +func ListArtifact(projectName, repoName string, opts ...ListFlags) (artifact.ListArtifactsOK, error) { ctx, client, err := utils.ContextWithClient() if err != nil { return artifact.ListArtifactsOK{}, err } + var listFlags ListFlags + if len(opts) > 0 { + listFlags = opts[0] + } response, err := client.Artifact.ListArtifacts(ctx, &artifact.ListArtifactsParams{ ProjectName: projectName, RepositoryName: repoName, + Page: &listFlags.Page, + PageSize: &listFlags.PageSize, + Q: &listFlags.Q, + Sort: &listFlags.Sort, }) if err != nil { return artifact.ListArtifactsOK{}, err diff --git a/pkg/api/user_handler.go b/pkg/api/user_handler.go index 4b599420..b1e8a7d6 100644 --- a/pkg/api/user_handler.go +++ b/pkg/api/user_handler.go @@ -67,17 +67,42 @@ func ElevateUser(userId int64) error { return nil } -func ListUsers() (*user.ListUsersOK, error) { +func ListUsers(opts ...ListFlags) (*user.ListUsersOK, error) { ctx, client, err := utils.ContextWithClient() if err != nil { return nil, err } + var listFlags ListFlags + if len(opts) > 0 { + listFlags = opts[0] + } - response, err := client.User.ListUsers(ctx, &user.ListUsersParams{}) - + response, err := client.User.ListUsers(ctx, &user.ListUsersParams{ + Page: &listFlags.Page, + PageSize: &listFlags.PageSize, + Q: &listFlags.Q, + Sort: &listFlags.Sort, + }) if err != nil { return nil, err } return response, nil } + +func GetUsersIdByName(userName string) (int64, error) { + var opts ListFlags + + u, err := ListUsers(opts) + if err != nil { + return 0, err + } + + for _, user := range u.Payload { + if user.Username == userName { + return user.UserID, nil + } + } + + return 0, err +} From c3773689b2b50c803e9da8a433da29a7f77a268a Mon Sep 17 00:00:00 2001 From: ALTHAF <114910365+Althaf66@users.noreply.github.com> Date: Sat, 23 Nov 2024 03:45:49 +0530 Subject: [PATCH 09/41] search command for project and repository (#174) * created search sub-command Signed-off-by: ALTHAF * fix merge conflicts and lint issue Signed-off-by: ALTHAF --------- Signed-off-by: ALTHAF Signed-off-by: ALTHAF <114910365+Althaf66@users.noreply.github.com> Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/project/cmd.go | 1 + cmd/harbor/root/project/search.go | 33 ++++++++++++++++++++ cmd/harbor/root/repository/cmd.go | 1 + cmd/harbor/root/repository/search.go | 33 ++++++++++++++++++++ pkg/api/project_handler.go | 14 +++++++++ pkg/api/repository_handler.go | 15 ++++++++++ pkg/views/project/list/view.go | 31 +++++++++++++++++++ pkg/views/repository/search/view.go | 45 ++++++++++++++++++++++++++++ 8 files changed, 173 insertions(+) create mode 100644 cmd/harbor/root/project/search.go create mode 100644 cmd/harbor/root/repository/search.go create mode 100644 pkg/views/repository/search/view.go diff --git a/cmd/harbor/root/project/cmd.go b/cmd/harbor/root/project/cmd.go index 3e279df2..39320601 100644 --- a/cmd/harbor/root/project/cmd.go +++ b/cmd/harbor/root/project/cmd.go @@ -17,6 +17,7 @@ func Project() *cobra.Command { ListProjectCommand(), ViewCommand(), LogsProjectCommmand(), + SearchProjectCommand(), ) return cmd diff --git a/cmd/harbor/root/project/search.go b/cmd/harbor/root/project/search.go new file mode 100644 index 00000000..89ed7a67 --- /dev/null +++ b/cmd/harbor/root/project/search.go @@ -0,0 +1,33 @@ +package project + +import ( + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/project/list" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func SearchProjectCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "search", + Short: "search project based on their names", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + + projects, err := api.SearchProject(args[0]) + if err != nil { + log.Fatalf("failed to get projects: %v", err) + } + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + utils.PrintPayloadInJSONFormat(projects) + return + } + + list.SearchProjects(projects.Payload.Project) + }, + } + return cmd +} diff --git a/cmd/harbor/root/repository/cmd.go b/cmd/harbor/root/repository/cmd.go index 3efa30b6..f5144078 100644 --- a/cmd/harbor/root/repository/cmd.go +++ b/cmd/harbor/root/repository/cmd.go @@ -12,6 +12,7 @@ func Repository() *cobra.Command { ListRepositoryCommand(), RepoInfoCmd(), RepoDeleteCmd(), + SearchRepoCmd(), ) return cmd diff --git a/cmd/harbor/root/repository/search.go b/cmd/harbor/root/repository/search.go new file mode 100644 index 00000000..9463741d --- /dev/null +++ b/cmd/harbor/root/repository/search.go @@ -0,0 +1,33 @@ +package repository + +import ( + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/repository/search" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func SearchRepoCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "search", + Short: "search repository based on their names", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + + repo, err := api.SearchRepository(args[0]) + if err != nil { + log.Fatalf("failed to get repositories: %v", err) + } + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + utils.PrintPayloadInJSONFormat(repo) + return + } + + search.SearchRepositories(repo.Payload.Repository) + }, + } + return cmd +} diff --git a/pkg/api/project_handler.go b/pkg/api/project_handler.go index 3ab9f022..5ebb3df2 100644 --- a/pkg/api/project_handler.go +++ b/pkg/api/project_handler.go @@ -4,6 +4,7 @@ import ( "strconv" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/project" + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/search" "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/project/create" @@ -102,6 +103,19 @@ func ListAllProjects(opts ...ListFlags) (project.ListProjectsOK, error) { return *response, nil } +func SearchProject(query string) (search.SearchOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return search.SearchOK{}, err + } + + response, err := client.Search.Search(ctx, &search.SearchParams{Q: query}) + if err != nil { + return search.SearchOK{}, err + } + return *response, nil +} + func LogsProject(projectName string) (*project.GetLogsOK, error) { ctx, client, err := utils.ContextWithClient() if err != nil { diff --git a/pkg/api/repository_handler.go b/pkg/api/repository_handler.go index 7e5662b4..187c174c 100644 --- a/pkg/api/repository_handler.go +++ b/pkg/api/repository_handler.go @@ -2,6 +2,7 @@ package api import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/client/repository" + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/search" "github.com/goharbor/harbor-cli/pkg/utils" log "github.com/sirupsen/logrus" ) @@ -52,3 +53,17 @@ func ListRepository(projectName string) (repository.ListRepositoriesOK, error) { return *response, nil } + +func SearchRepository(query string) (search.SearchOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return search.SearchOK{}, err + } + + response, err := client.Search.Search(ctx, &search.SearchParams{Q: query}) + if err != nil { + return search.SearchOK{}, err + } + + return *response, nil +} diff --git a/pkg/views/project/list/view.go b/pkg/views/project/list/view.go index 51dbb487..f91594bf 100644 --- a/pkg/views/project/list/view.go +++ b/pkg/views/project/list/view.go @@ -52,3 +52,34 @@ func ListProjects(projects []*models.Project) { os.Exit(1) } } + +func SearchProjects(projects []*models.Project) { + var rows []table.Row + for _, project := range projects { + accessLevel := project.Metadata.Public + if accessLevel != "true" { + accessLevel = "private" + } else { + accessLevel = "public" + } + projectType := "project" + if project.RegistryID != 0 { + projectType = "proxy cache" + } + createdTime, _ := utils.FormatCreatedTime(project.CreationTime.String()) + rows = append(rows, table.Row{ + strconv.FormatInt(int64(project.ProjectID), 10), // ProjectID + project.Name, // Project Name + accessLevel, // Access Level + projectType, // Type + strconv.FormatInt(project.RepoCount, 10), + createdTime, // Creation Time + }) + } + m := tablelist.NewModel(columns, rows, len(rows)) + + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/pkg/views/repository/search/view.go b/pkg/views/repository/search/view.go new file mode 100644 index 00000000..508adcf5 --- /dev/null +++ b/pkg/views/repository/search/view.go @@ -0,0 +1,45 @@ +package search + +import ( + "fmt" + "os" + "strconv" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "Repository Name", Width: 30}, + {Title: "Project Id", Width: 11}, + {Title: "Project Name", Width: 16}, + {Title: "Access Level", Width: 12}, + {Title: "Artifact Count", Width: 14}, + {Title: "Pull Count", Width: 12}, +} + +func SearchRepositories(repos []*models.SearchRepository) { + var rows []table.Row + for _, repo := range repos { + accessLevel := "public" + if !repo.ProjectPublic { + accessLevel = "private" + } + rows = append(rows, table.Row{ + repo.RepositoryName, + fmt.Sprintf("%d", repo.ProjectID), + repo.ProjectName, + accessLevel, + fmt.Sprintf("%d", repo.ArtifactCount), + strconv.FormatInt(repo.PullCount, 10), + }) + } + + m := tablelist.NewModel(columns, rows, len(rows)) + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} From 185ff88328333c48f6ff1ba84c2c839347ee6041 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach <45457307+qcserestipy@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:45:13 +0100 Subject: [PATCH 10/41] feat: Config flag error ignored during login #251 (#259) * Added configpath to loginview; Added configpath validator; Added configpath flag check to determine if given Signed-off-by: qcserestipy * Added update credentials function Signed-off-by: qcserestipy * Removed config file from login view; moved credential update or creation to runLogin function Signed-off-by: qcserestipy * Added PersistentPreRun function to handle config init; Added data file logic that keeps track of the last active config; Made DataFile and ConfigFile Available through global pointer. Adjusted harbor client to retrieve credentials through config pointer; Added funtionality to login run to update credentials after login Signed-off-by: qcserestipy * Enhanced logging for login subcommand Signed-off-by: qcserestipy * Improved handling of initConfig function; introduced HARBOR_CLI_CONFIG flag Signed-off-by: qcserestipy * Improved Data and Config Pointer error handling; Included Mutex functionality for Config and Data Pointer Signed-off-by: qcserestipy * Added custom sync.Once to handle concurrent config environments in tests; added config tests; adapted login tests to work with new config management Signed-off-by: Patrick Eschenbach * Added custom sync.Once to handle concurrent config environments in tests; added config tests; adapted login tests to work with new config management Signed-off-by: Patrick Eschenbach --------- Signed-off-by: qcserestipy Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/cmd.go | 73 ++----- cmd/harbor/root/login.go | 31 ++- pkg/utils/client.go | 15 +- pkg/utils/config.go | 430 ++++++++++++++++++++++++++++++++++---- pkg/utils/helper.go | 6 + pkg/views/login/create.go | 4 +- test/e2e/config_test.go | 146 +++++++++++++ test/e2e/login_test.go | 32 +-- 8 files changed, 617 insertions(+), 120 deletions(-) create mode 100644 test/e2e/config_test.go diff --git a/cmd/harbor/root/cmd.go b/cmd/harbor/root/cmd.go index 94c4f8a3..a975dbeb 100644 --- a/cmd/harbor/root/cmd.go +++ b/cmd/harbor/root/cmd.go @@ -2,8 +2,6 @@ package root import ( "fmt" - "log" - "os" "github.com/goharbor/harbor-cli/cmd/harbor/root/artifact" "github.com/goharbor/harbor-cli/cmd/harbor/root/project" @@ -21,58 +19,7 @@ var ( verbose bool ) -func InitConfig() { - viper.SetConfigType("yaml") - - // cfgFile = viper.GetStering("config") - viper.SetConfigFile(cfgFile) - - if cfgFile != utils.DefaultConfigPath { - viper.SetConfigFile(cfgFile) - } else { - stat, err := os.Stat(utils.DefaultConfigPath) - if !os.IsNotExist(err) && stat.Size() == 0 { - log.Println("Config file is empty, creating a new one") - } - - if os.IsNotExist(err) { - log.Printf("Config file not found at %s, creating a new one", cfgFile) - } - - if os.IsNotExist(err) || (!os.IsNotExist(err) && stat.Size() == 0) { - if _, err := os.Stat(utils.HarborFolder); os.IsNotExist(err) { - // Create the parent directory if it doesn't exist - - fmt.Println("Creating config file", utils.HarborFolder) - if err := os.MkdirAll(utils.HarborFolder, os.ModePerm); err != nil { - log.Fatal(err) - } - } - err = utils.CreateConfigFile() - - if err != nil { - log.Fatal(err) - } - - err = utils.AddCredentialsToConfigFile(utils.Credential{}, cfgFile) - - if err != nil { - log.Fatal(err) - } - - log.Printf("Config file created at %s", cfgFile) - } - } - - if err := viper.ReadInConfig(); err != nil { - log.Fatalf("Error reading config file: %s", err) - } - -} - func RootCmd() *cobra.Command { - utils.SetLocation() - root := &cobra.Command{ Use: "harbor", Short: "Official Harbor CLI", @@ -85,15 +32,18 @@ harbor // Display help about the command: harbor help `, - // RunE: func(cmd *cobra.Command, args []string) error { - - // }, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Determine if --config was explicitly set + userSpecifiedConfig := cmd.Flags().Changed("config") + // Initialize configuration + utils.InitConfig(cfgFile, userSpecifiedConfig) + + return nil + }, } - cobra.OnInitialize(InitConfig) - root.PersistentFlags().StringVarP(&output, "output-format", "o", "", "Output format. One of: json|yaml") - root.PersistentFlags().StringVar(&cfgFile, "config", utils.DefaultConfigPath, "config file (default is $HOME/.harbor/config.yaml)") + root.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.config/harbor-cli/config.yaml)") root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") err := viper.BindPFlag("output-format", root.PersistentFlags().Lookup("output-format")) @@ -101,6 +51,11 @@ harbor help fmt.Println(err.Error()) } + err = viper.BindPFlag("config", root.PersistentFlags().Lookup("config")) + if err != nil { + fmt.Println(err.Error()) + } + root.AddCommand( versionCommand(), LoginCommand(), diff --git a/cmd/harbor/root/login.go b/cmd/harbor/root/login.go index 316ded87..78750fbe 100644 --- a/cmd/harbor/root/login.go +++ b/cmd/harbor/root/login.go @@ -9,6 +9,7 @@ import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/client/user" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/login" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "golang.org/x/term" ) @@ -115,9 +116,37 @@ func runLogin(opts login.LoginView) error { Password: opts.Password, ServerAddress: opts.Server, } + harborData, err := utils.GetCurrentHarborData() + if err != nil { + return fmt.Errorf("failed to get current harbor data: %s", err) + } + configPath := harborData.ConfigPath + log.Debugf("Checking if credentials already exist in the config file...") + existingCred, err := utils.GetCredentials(opts.Name) + if err == nil { + if existingCred.Username == opts.Username && existingCred.ServerAddress == opts.Server { + if existingCred.Password == opts.Password { + log.Warn("Credentials already exist in the config file. They were not added again.") + return nil + } else { + log.Warn("Credentials already exist in the config file but the password is different. Updating the password.") + if err = utils.UpdateCredentialsInConfigFile(cred, configPath); err != nil { + log.Fatalf("failed to update the credential: %s", err) + } + return nil + } + } else { + log.Warn("Credentials already exist in the config file but more than one field was different. Updating the credentials.") + if err = utils.UpdateCredentialsInConfigFile(cred, configPath); err != nil { + log.Fatalf("failed to update the credential: %s", err) + } + return nil + } + } - if err = utils.AddCredentialsToConfigFile(cred, utils.DefaultConfigPath); err != nil { + if err = utils.AddCredentialsToConfigFile(cred, configPath); err != nil { return fmt.Errorf("failed to store the credential: %s", err) } + log.Debugf("Credentials successfully added to the config file.") return nil } diff --git a/pkg/utils/client.go b/pkg/utils/client.go index 4da1e0aa..388e380a 100644 --- a/pkg/utils/client.go +++ b/pkg/utils/client.go @@ -9,7 +9,6 @@ import ( "github.com/goharbor/go-client/pkg/harbor" v2client "github.com/goharbor/go-client/pkg/sdk/v2.0/client" log "github.com/sirupsen/logrus" - "github.com/spf13/viper" ) var ( @@ -20,12 +19,24 @@ var ( func GetClient() (*v2client.HarborAPI, error) { clientOnce.Do(func() { - credentialName := viper.GetString("current-credential-name") + config, err := GetCurrentHarborConfig() + if err != nil { + clientErr = fmt.Errorf("failed to get current credential name: %v", err) + return + } + credentialName := config.CurrentCredentialName + if credentialName == "" { + clientErr = fmt.Errorf("current-credential-name is not set in config file") + return + } + clientInstance = GetClientByCredentialName(credentialName) if clientErr != nil { log.Errorf("failed to initialize client: %v", clientErr) + return } }) + return clientInstance, clientErr } diff --git a/pkg/utils/config.go b/pkg/utils/config.go index 1bab413f..ec83542c 100644 --- a/pkg/utils/config.go +++ b/pkg/utils/config.go @@ -1,9 +1,13 @@ package utils import ( - "log" + "errors" + "fmt" "os" "path/filepath" + "sync" + + log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) @@ -16,87 +20,427 @@ type Credential struct { } type HarborConfig struct { - CurrentCredentialName string `yaml:"current-credential-name"` - Credentials []Credential `yaml:"credentials"` + CurrentCredentialName string `mapstructure:"current-credential-name" yaml:"current-credential-name"` + Credentials []Credential `mapstructure:"credentials" yaml:"credentials"` +} + +type HarborData struct { + ConfigPath string `mapstructure:"configpath" yaml:"configpath"` +} + +type Once struct { + once sync.Once +} + +func (o *Once) Do(f func()) { + o.once.Do(f) +} + +func (o *Once) Reset() { + o.once = sync.Once{} } var ( - HarborFolder string - DefaultConfigPath string + CurrentHarborData *HarborData + CurrentHarborConfig *HarborConfig + configMutex sync.RWMutex + configInitError error ) -func SetLocation() { - home, err := os.UserHomeDir() +var ConfigInitialization = &Once{} + +func InitConfig(cfgFile string, userSpecifiedConfig bool) { + ConfigInitialization.Do(func() { + harborDataPath, harborDataDir := GetDataPaths() + harborConfigPath, err := DetermineConfigPath(cfgFile, userSpecifiedConfig) + if err != nil { + configInitError = err + log.Fatalf("%v", err) + } + + // Ensure data directory exists + if err := os.MkdirAll(harborDataDir, os.ModePerm); err != nil { + configInitError = fmt.Errorf("failed to create data directory: %w", err) + log.Fatalf("%v", configInitError) + } + + // Update or create data file + if err := ApplyDataFile(harborDataPath, harborConfigPath); err != nil { + configInitError = err + log.Fatalf("%v", err) + } + + // Ensure config file exists + if err := EnsureConfigFileExists(harborConfigPath); err != nil { + configInitError = err + log.Fatalf("%v", err) + } + + // Read and unmarshal the config file + v, err := ReadConfig(harborConfigPath) + if err != nil { + configInitError = err + log.Fatalf("%v", err) + } + + var harborConfig HarborConfig + if err := v.Unmarshal(&harborConfig); err != nil { + configInitError = fmt.Errorf("failed to unmarshal config file: %w", err) + log.Fatalf("%v", configInitError) + } + + configMutex.Lock() + defer configMutex.Unlock() + CurrentHarborConfig = &harborConfig + CurrentHarborData = &HarborData{ConfigPath: harborConfigPath} + }) +} + +// Helper function to get data paths +func GetDataPaths() (harborDataPath string, harborDataDir string) { + xdgDataHome := os.Getenv("XDG_DATA_HOME") + if xdgDataHome == "" { + home, err := os.UserHomeDir() + if err != nil { + log.Fatalf("Unable to determine user home directory: %v", err) + } + xdgDataHome = filepath.Join(home, ".local", "share") + } + harborDataDir = filepath.Join(xdgDataHome, "harbor-cli") + harborDataPath = filepath.Join(harborDataDir, "data.yaml") + return +} + +// Helper function to determine the config path +func DetermineConfigPath(cfgFile string, userSpecifiedConfig bool) (string, error) { + var harborConfigPath string + var err error + + // 1. Check if user specified --config + if userSpecifiedConfig && cfgFile != "" { + harborConfigPath, err = filepath.Abs(cfgFile) + if err != nil { + return "", fmt.Errorf("failed to resolve absolute path for config file: %w", err) + } + return harborConfigPath, nil + } + // 2. Check HARBOR_CLI_CONFIG environment variable + harborEnvVar := os.Getenv("HARBOR_CLI_CONFIG") + if harborEnvVar != "" { + harborConfigPath, err = filepath.Abs(harborEnvVar) + if err != nil { + return "", fmt.Errorf("failed to resolve absolute path for config file from HARBOR_CLI_CONFIG: %w", err) + } + return harborConfigPath, nil + } + + // 3. Use default XDG config path + xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") + if xdgConfigHome == "" { + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("unable to determine user home directory: %w", err) + } + xdgConfigHome = filepath.Join(home, ".config") + } + harborConfigPath, err = filepath.Abs(filepath.Join(xdgConfigHome, "harbor-cli", "config.yaml")) if err != nil { - log.Fatal(err) + return "", fmt.Errorf("failed to resolve absolute path for default config file: %w", err) + } + return harborConfigPath, nil +} + +// Helper function to ensure config file exists +func EnsureConfigFileExists(harborConfigPath string) error { + // Ensure parent directory exists + configDir := filepath.Dir(harborConfigPath) + if err := os.MkdirAll(configDir, os.ModePerm); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + // Create config file if it doesn't exist + if _, err := os.Stat(harborConfigPath); os.IsNotExist(err) { + if err := CreateConfigFile(harborConfigPath); err != nil { + return fmt.Errorf("failed to create config file: %w", err) + } + } + return nil +} + +// Helper function to read the config file using Viper +func ReadConfig(harborConfigPath string) (*viper.Viper, error) { + v := viper.New() + v.SetConfigFile(harborConfigPath) + v.SetConfigType("yaml") + if err := v.ReadInConfig(); err != nil { + return nil, fmt.Errorf("error reading config file: %w. Please ensure the config file exists.", err) } - HarborFolder = filepath.Join(home, ".harbor") - DefaultConfigPath = filepath.Join(HarborFolder, "config.yaml") + return v, nil } -func (hc *HarborConfig) GetCurrentCredentialName() string { - return hc.CurrentCredentialName +func GetCurrentHarborConfig() (*HarborConfig, error) { + ConfigInitialization.Do(func() { + // No action needed; InitConfig should have been called before + }) + + if configInitError != nil { + return nil, fmt.Errorf("initialization error: %w", configInitError) + } + + configMutex.RLock() + defer configMutex.RUnlock() + + if CurrentHarborConfig == nil { + return nil, errors.New("configuration is not yet initialized") + } + + return CurrentHarborConfig, nil } -func CreateConfigFile() error { - if _, err := os.Stat(DefaultConfigPath); os.IsNotExist(err) { - if _, err := os.Create(DefaultConfigPath); err != nil { - return err +func GetCurrentHarborData() (*HarborData, error) { + ConfigInitialization.Do(func() { + // No action needed; initialization should have been called before + }) + + if configInitError != nil { + return nil, fmt.Errorf("initialization error: %w", configInitError) + } + + configMutex.RLock() + defer configMutex.RUnlock() + + if CurrentHarborData == nil { + return nil, errors.New("HarborData is not yet initialized") + } + + return CurrentHarborData, nil +} + +func CreateDataFile(dataFilePath string, initialConfigPath string) error { + if _, err := os.Stat(dataFilePath); os.IsNotExist(err) { + dataDir := filepath.Dir(dataFilePath) + if err := os.MkdirAll(dataDir, os.ModePerm); err != nil { + log.Fatalf("Failed to create data directory: %v", err) + } + + absConfigPath, err := filepath.Abs(initialConfigPath) + if err != nil { + log.Fatalf("Failed to resolve absolute path for config file: %v", err) } + + dataFile := HarborData{ + ConfigPath: absConfigPath, + } + + v := viper.New() + v.SetConfigType("yaml") + v.Set("configPath", dataFile.ConfigPath) + + if err := v.WriteConfigAs(dataFilePath); err != nil { + log.Fatalf("Failed to write data file: %v", err) + } + + log.Infof("Data file created at %s with configPath: %s", dataFilePath, dataFile.ConfigPath) + } else if err != nil { + log.Fatalf("Error checking data file: %v", err) } + return nil } -func AddCredentialsToConfigFile(credential Credential, configPath string) error { - if _, err := os.Stat(configPath); os.IsNotExist(err) { - return err +func ReadDataFile(dataFilePath string) (HarborData, error) { + var dataFile HarborData + + v := viper.New() + v.SetConfigType("yaml") + v.SetConfigFile(dataFilePath) + + if err := v.ReadInConfig(); err != nil { + return dataFile, fmt.Errorf("failed to read data file: %v", err) } - viper.SetConfigFile(configPath) - err := viper.ReadInConfig() - if err != nil { - return err + dataFile.ConfigPath = v.GetString("configPath") + + return dataFile, nil +} + +// Helper function to update or create the data file +func ApplyDataFile(harborDataPath, harborConfigPath string) error { + dataFileContent, err := ReadDataFile(harborDataPath) + if err == nil { + if dataFileContent.ConfigPath != harborConfigPath { + if err := UpdateDataFile(harborDataPath, harborConfigPath); err != nil { + return fmt.Errorf("failed to update data file: %w", err) + } + } else if dataFileContent.ConfigPath == "" { + if err := CreateDataFile(harborDataPath, harborConfigPath); err != nil { + return fmt.Errorf("failed to create data file: %w", err) + } + } else { + log.Debugf("Data file already exists with the same config path: %s", harborConfigPath) + } + } else { + // Data file does not exist, create it + if err := CreateDataFile(harborDataPath, harborConfigPath); err != nil { + return fmt.Errorf("failed to create data file: %w", err) + } } + return nil +} - c := HarborConfig{} - err = viper.Unmarshal(&c) +func UpdateDataFile(dataFilePath string, newConfigPath string) error { + if _, err := os.Stat(dataFilePath); os.IsNotExist(err) { + log.Fatalf("data file does not exist at %s", dataFilePath) + } else if err != nil { + log.Fatalf("error checking data file: %v", err) + } + + absConfigPath, err := filepath.Abs(newConfigPath) if err != nil { - return err + log.Fatalf("failed to resolve absolute path for new config file: %v", err) } - if c.Credentials == nil { - c.Credentials = []Credential{} + v := viper.New() + v.SetConfigType("yaml") + v.SetConfigFile(dataFilePath) + + if err := v.ReadInConfig(); err != nil { + log.Fatalf("failed to read existing data file: %v", err) } - c.Credentials = append(c.Credentials, credential) - c.CurrentCredentialName = credential.Name + v.Set("configPath", absConfigPath) - viper.Set("current-credential-name", credential.Name) - viper.Set("credentials", c.Credentials) - err = viper.WriteConfig() - if err != nil { - return err + if err := v.WriteConfig(); err != nil { + log.Fatalf("failed to write updated data file: %v", err) } + + log.Infof("Data file at %s updated with new configPath: %s", dataFilePath, absConfigPath) return nil +} + +func CreateConfigFile(configPath string) error { + if _, err := os.Stat(configPath); os.IsNotExist(err) { + configDir := filepath.Dir(configPath) + if err := os.MkdirAll(configDir, os.ModePerm); err != nil { + log.Fatalf("failed to create config directory: %v", err) + } + + v := viper.New() + v.SetConfigType("yaml") + defaultConfig := HarborConfig{ + CurrentCredentialName: "", + Credentials: []Credential{}, + } + + v.Set("current-credential-name", defaultConfig.CurrentCredentialName) + v.Set("credentials", defaultConfig.Credentials) + + if err := v.WriteConfigAs(configPath); err != nil { + log.Fatalf("failed to write config file: %v", err) + } + + log.Infof("Config file created at %s", configPath) + } else if err != nil { + log.Fatalf("error checking config file: %v", err) + } + + return nil } func GetCredentials(credentialName string) (Credential, error) { - err := viper.ReadInConfig() + currentConfig, err := GetCurrentHarborConfig() if err != nil { - return Credential{}, err + return Credential{}, fmt.Errorf("failed to get current Harbor configuration: %w", err) } - c := HarborConfig{} - err = viper.Unmarshal(&c) - if err != nil { - return Credential{}, err + if currentConfig == nil || currentConfig.Credentials == nil { + return Credential{}, errors.New("current Harbor configuration or credentials are not initialized") } - for _, cred := range c.Credentials { + for _, cred := range currentConfig.Credentials { if cred.Name == credentialName { return cred, nil } } - return Credential{}, nil + + return Credential{}, fmt.Errorf("credential with name '%s' not found", credentialName) +} + +func AddCredentialsToConfigFile(credential Credential, configPath string) error { + if _, err := os.Stat(configPath); os.IsNotExist(err) { + log.Fatalf("config file does not exist at %s", configPath) + } else if err != nil { + log.Fatalf("error checking config file: %v", err) + } + + v := viper.New() + v.SetConfigFile(configPath) + v.SetConfigType("yaml") + + if err := v.ReadInConfig(); err != nil { + log.Fatalf("failed to read config file: %v", err) + } + + var c HarborConfig + if err := v.Unmarshal(&c); err != nil { + log.Fatalf("failed to unmarshal config file: %v", err) + } + + c.Credentials = append(c.Credentials, credential) + c.CurrentCredentialName = credential.Name + + v.Set("current-credential-name", c.CurrentCredentialName) + v.Set("credentials", c.Credentials) + + if err := v.WriteConfig(); err != nil { + log.Fatalf("failed to write updated config file: %v", err) + } + + log.Infof("Added credential '%s' to config file at %s", credential.Name, configPath) + return nil +} + +func UpdateCredentialsInConfigFile(updatedCredential Credential, configPath string) error { + if _, err := os.Stat(configPath); os.IsNotExist(err) { + log.Fatalf("config file does not exist at %s", configPath) + } else if err != nil { + log.Fatalf("error checking config file: %v", err) + } + + v := viper.New() + v.SetConfigFile(configPath) + v.SetConfigType("yaml") + + if err := v.ReadInConfig(); err != nil { + log.Fatalf("failed to read config file: %v", err) + } + + var c HarborConfig + if err := v.Unmarshal(&c); err != nil { + log.Fatalf("failed to unmarshal config file: %v", err) + } + + updated := false + for i, cred := range c.Credentials { + if cred.Name == updatedCredential.Name { + c.Credentials[i] = updatedCredential + updated = true + break + } + } + + if !updated { + log.Fatalf("credential with name '%s' not found", updatedCredential.Name) + } + + v.Set("current-credential-name", c.CurrentCredentialName) + v.Set("credentials", c.Credentials) + + if err := v.WriteConfig(); err != nil { + log.Fatalf("failed to write updated config file: %v", err) + } + + log.Infof("Updated credential '%s' in config file at %s", updatedCredential.Name, configPath) + return nil } diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index 31f06c9b..6421243b 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -56,6 +56,12 @@ func ValidateEmail(email string) bool { return re.MatchString(email) } +func ValidateConfigPath(configPath string) bool { + pattern := `^[\w./-]{1,255}\.(yaml|yml)$` + re := regexp.MustCompile(pattern) + return re.MatchString(configPath) +} + func ValidateFL(name string) bool { pattern := `^[A-Za-z]{1,20}\s[A-Za-z]{1,20}$` re := regexp.MustCompile(pattern) diff --git a/pkg/views/login/create.go b/pkg/views/login/create.go index 0838845d..ad653521 100644 --- a/pkg/views/login/create.go +++ b/pkg/views/login/create.go @@ -16,6 +16,7 @@ type LoginView struct { Username string Password string Name string + Config string } func CreateView(loginView *LoginView) { @@ -82,7 +83,8 @@ func CreateView(loginView *LoginView) { return nil }), ), - ).WithTheme(theme).Run() + ).WithTheme(theme). + Run() if err != nil { log.Fatal(err) } diff --git a/test/e2e/config_test.go b/test/e2e/config_test.go new file mode 100644 index 00000000..239491c5 --- /dev/null +++ b/test/e2e/config_test.go @@ -0,0 +1,146 @@ +package e2e + +import ( + "os" + "path/filepath" + "sync" + "testing" + + "github.com/goharbor/harbor-cli/cmd/harbor/root" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/stretchr/testify/assert" +) + +var envMutex sync.Mutex + +func safeSetEnv(key, value string) { + envMutex.Lock() + defer envMutex.Unlock() + os.Setenv(key, value) +} + +func safeUnsetEnv(key string) { + envMutex.Lock() + defer envMutex.Unlock() + os.Unsetenv(key) +} + +func ConfigCleanup(t *testing.T, data *utils.HarborData) { + if data != nil && data.ConfigPath != "" { + err := os.Remove(data.ConfigPath) + if err != nil && !os.IsNotExist(err) { + t.Fatalf("Failed to clean up test config file: %v", err) + } + } + if os.Getenv("XDG_CONFIG_HOME") != "" { + err := os.RemoveAll(os.Getenv("XDG_CONFIG_HOME")) + if err != nil { + t.Fatalf("Failed to clean up test config directory: %v", err) + } + } + if os.Getenv("XDG_DATA_HOME") != "" { + err := os.RemoveAll(os.Getenv("XDG_DATA_HOME")) + if err != nil { + t.Fatalf("Failed to clean up test data directory: %v", err) + } + } + safeUnsetEnv("HARBOR_CLI_CONFIG") + safeUnsetEnv("XDG_CONFIG_HOME") + safeUnsetEnv("XDG_DATA_HOME") + data = nil +} + +func Initialize(t *testing.T, tempDir string) *utils.HarborData { + utils.ConfigInitialization.Reset() // Reset sync.Once for the test + safeSetEnv("XDG_DATA_HOME", filepath.Join(tempDir, ".data")) + utils.InitConfig(filepath.Join(tempDir, ".config", "config.yaml"), true) + cds := root.RootCmd() + err := cds.Execute() + assert.NoError(t, err, "Expected no error for Root command") + assert.NoError(t, err, "Expected no error for Root command execution") + + currentData, err := utils.GetCurrentHarborData() + assert.NoError(t, err, "Expected no error when fetching HarborData") + return currentData +} + +func Test_Config_EnvVar(t *testing.T) { + utils.ConfigInitialization.Reset() // Reset sync.Once for the test + tempDir := t.TempDir() + safeSetEnv("HARBOR_CLI_CONFIG", filepath.Join(tempDir, "config.yaml")) + safeSetEnv("XDG_DATA_HOME", filepath.Join(tempDir, ".data")) + utils.InitConfig("", false) + cds := root.RootCmd() + err := cds.Execute() + assert.NoError(t, err, "Expected no error for Root command") + assert.NoError(t, err, "Expected no error for Root command execution") + + currentData, err := utils.GetCurrentHarborData() + assert.NoError(t, err, "Expected no error when fetching HarborData") + defer ConfigCleanup(t, currentData) + + currentConfig, err := utils.GetCurrentHarborConfig() + assert.NoError(t, err, "Expected no error when fetching HarborConfig") + assert.NotNil(t, currentConfig, "Configuration should not be nil") + assert.NotNil(t, currentConfig.CurrentCredentialName, "CurrentCredentialName should not be nil") + assert.NotNil(t, currentConfig.Credentials, "Credentials should not be nil") + assert.NotNil(t, currentData.ConfigPath, "ConfigPath should not be nil") +} + +func Test_Config_Vanilla(t *testing.T) { + utils.ConfigInitialization.Reset() // Reset sync.Once for the test + utils.InitConfig("", false) + cds := root.RootCmd() + err := cds.Execute() + assert.NoError(t, err, "Expected no error for Root command") + assert.NoError(t, err, "Expected no error for Root command execution") + currentData, err := utils.GetCurrentHarborData() + assert.NoError(t, err, "Expected no error when fetching HarborData") + defer ConfigCleanup(t, currentData) + + currentConfig, err := utils.GetCurrentHarborConfig() + assert.NoError(t, err, "Expected no error when fetching HarborConfig") + assert.NotNil(t, currentConfig, "Configuration should not be nil") + assert.NotNil(t, currentConfig.CurrentCredentialName, "CurrentCredentialName should not be nil") + assert.NotNil(t, currentConfig.Credentials, "Credentials should not be nil") + assert.NotNil(t, currentData.ConfigPath, "ConfigPath should not be nil") +} + +func Test_Config_Xdg(t *testing.T) { + utils.ConfigInitialization.Reset() // Reset sync.Once for the test + tempDir := t.TempDir() + safeSetEnv("HARBOR_CLI_CONFIG", filepath.Join(tempDir, "config.yaml")) + safeSetEnv("XDG_CONFIG_HOME", filepath.Join(tempDir, ".config")) + safeSetEnv("XDG_DATA_HOME", filepath.Join(tempDir, ".data")) + utils.InitConfig("", false) + cds := root.RootCmd() + err := cds.Execute() + assert.NoError(t, err, "Expected no error for Root command") + assert.NoError(t, err, "Expected no error for Root command execution") + + currentData, err := utils.GetCurrentHarborData() + assert.NoError(t, err, "Expected no error when fetching HarborData") + defer ConfigCleanup(t, currentData) + + currentConfig, err := utils.GetCurrentHarborConfig() + assert.NoError(t, err, "Expected no error when fetching HarborConfig") + assert.NotNil(t, currentConfig, "Configuration should not be nil") + assert.NotNil(t, currentConfig.CurrentCredentialName, "CurrentCredentialName should not be nil") + assert.NotNil(t, currentConfig.Credentials, "Credentials should not be nil") + assert.NotNil(t, currentData.ConfigPath, "ConfigPath should not be nil") +} + +func Test_Config_Flag(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + + testConfigFile := filepath.Join(tempDir, "config.yaml") + utils.InitConfig(testConfigFile, true) + currentConfig, err := utils.GetCurrentHarborConfig() + assert.NoError(t, err, "Expected no error when fetching HarborConfig") + assert.NotNil(t, currentConfig, "Configuration should not be nil") + assert.NotNil(t, currentConfig.CurrentCredentialName, "CurrentCredentialName should not be nil") + assert.NotNil(t, currentConfig.Credentials, "Credentials should not be nil") + assert.NotNil(t, data.ConfigPath, "ConfigPath should not be nil") +} diff --git a/test/e2e/login_test.go b/test/e2e/login_test.go index 23d5dd51..e3869057 100644 --- a/test/e2e/login_test.go +++ b/test/e2e/login_test.go @@ -7,15 +7,10 @@ import ( "github.com/stretchr/testify/assert" ) -func initialize(t *testing.T) { - cds := root.RootCmd() - err := cds.Execute() - assert.NoError(t, err, "Expected no error for Root command") -} - func Test_Login_Success(t *testing.T) { - initialize(t) // Initialize the root command - + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) cmd := root.LoginCommand() validServerAddresses := []string{ "http://demo.goharbor.io:80", @@ -41,9 +36,12 @@ func Test_Login_Success(t *testing.T) { } func Test_Login_Failure_WrongServer(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + cmd := root.LoginCommand() - args := []string{"wrongserver"} - cmd.SetArgs(args) + cmd.SetArgs([]string{"wrongserver"}) assert.NoError(t, cmd.Flags().Set("name", "test")) assert.NoError(t, cmd.Flags().Set("username", "harbor-cli")) @@ -54,9 +52,12 @@ func Test_Login_Failure_WrongServer(t *testing.T) { } func Test_Login_Failure_WrongUsername(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + cmd := root.LoginCommand() - args := []string{"http://demo.goharbor.io"} - cmd.SetArgs(args) + cmd.SetArgs([]string{"http://demo.goharbor.io"}) assert.NoError(t, cmd.Flags().Set("name", "test")) assert.NoError(t, cmd.Flags().Set("username", "does-not-exist")) @@ -67,9 +68,12 @@ func Test_Login_Failure_WrongUsername(t *testing.T) { } func Test_Login_Failure_WrongPassword(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + cmd := root.LoginCommand() - args := []string{"http://demo.goharbor.io"} - cmd.SetArgs(args) + cmd.SetArgs([]string{"http://demo.goharbor.io"}) assert.NoError(t, cmd.Flags().Set("name", "test")) assert.NoError(t, cmd.Flags().Set("username", "admin")) From 3b7925786b0ffda746ad30f5d8e0decfef063406 Mon Sep 17 00:00:00 2001 From: Adwait Godbole <68469605+adwait-godbole@users.noreply.github.com> Date: Tue, 26 Nov 2024 20:15:39 +0530 Subject: [PATCH 11/41] add '--force' flag to delete non-empty projects (#252) Signed-off-by: adwait-godbole Signed-off-by: Vadim Bauer Co-authored-by: Vadim Bauer Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/project/delete.go | 8 ++++++-- pkg/api/project_handler.go | 26 ++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/cmd/harbor/root/project/delete.go b/cmd/harbor/root/project/delete.go index b09447cb..3e0e5b98 100644 --- a/cmd/harbor/root/project/delete.go +++ b/cmd/harbor/root/project/delete.go @@ -9,6 +9,7 @@ import ( // DeleteProjectCommand creates a new `harbor delete project` command func DeleteProjectCommand() *cobra.Command { + var forceDelete bool cmd := &cobra.Command{ Use: "delete", @@ -18,10 +19,10 @@ func DeleteProjectCommand() *cobra.Command { var err error if len(args) > 0 { - err = api.DeleteProject(args[0]) + err = api.DeleteProject(args[0], forceDelete) } else { projectName := prompt.GetProjectNameFromUser() - err = api.DeleteProject(projectName) + err = api.DeleteProject(projectName, forceDelete) } if err != nil { log.Errorf("failed to delete project: %v", err) @@ -29,5 +30,8 @@ func DeleteProjectCommand() *cobra.Command { }, } + flags := cmd.Flags() + flags.BoolVar(&forceDelete, "force", false, "Deletes all repositories and artifacts within the project") + return cmd } diff --git a/pkg/api/project_handler.go b/pkg/api/project_handler.go index 5ebb3df2..5a06e9da 100644 --- a/pkg/api/project_handler.go +++ b/pkg/api/project_handler.go @@ -4,6 +4,7 @@ import ( "strconv" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/project" + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/repository" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/search" "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/utils" @@ -55,19 +56,40 @@ func GetProject(projectName string) error { return nil } -func DeleteProject(projectName string) error { +func DeleteProject(projectName string, forceDelete bool) error { ctx, client, err := utils.ContextWithClient() if err != nil { return err } + if forceDelete { + var resp repository.ListRepositoriesOK + + resp, err = ListRepository(projectName) + + if err != nil { + log.Errorf("failed to list repositories: %v", err) + return err + } + + for _, repo := range resp.Payload { + _, repoName := utils.ParseProjectRepo(repo.Name) + err = RepoDelete(projectName, repoName) + + if err != nil { + log.Errorf("failed to delete repository: %v", err) + return err + } + } + } + _, err = client.Project.DeleteProject(ctx, &project.DeleteProjectParams{ProjectNameOrID: projectName}) if err != nil { return err } - log.Info("project deleted successfully") + log.Infof("Project %s deleted successfully", projectName) return nil } From 8c4970e8c013095d3b24ee2d943ff06f0709a8ee Mon Sep 17 00:00:00 2001 From: Alan Tang Date: Tue, 26 Nov 2024 22:46:19 +0800 Subject: [PATCH 12/41] Support yaml output (#241) * Support yaml output for 'registry list' Signed-off-by: JianMinTang * Use gofmt to format all code Signed-off-by: JianMinTang * fix: Support YAML output for additional commands Signed-off-by: JianMinTang * fix: Support YAML format on artiface and repo command Signed-off-by: JianMinTang * fix: Implement a generic function to format output Signed-off-by: JianMinTang * chore: fix the problem about golangci-lint Signed-off-by: JianMinTang * AutoGenerate credential name in login (#250) * generate credential name Signed-off-by: bupd * feat: add support for the password-stdin flag in login flow Signed-off-by: karanngi * fix deps - fixes dependencies Signed-off-by: bupd * return stdout for tests Signed-off-by: bupd * update workflow Signed-off-by: bupd --------- Signed-off-by: bupd Signed-off-by: karanngi Co-authored-by: karanngi Signed-off-by: JianMinTang * print test output to screen (#254) print test output to screen Signed-off-by: JianMinTang * Support table format for repo view and add some comments on repo list Signed-off-by: JianMinTang Add more detail on repo view Signed-off-by: JianMinTang Support table format on registry view Signed-off-by: JianMinTang Support table format on project view Signed-off-by: JianMinTang Fixed tags list Signed-off-by: JianMinTang Support table format and YAML/JSON output on artifact view Signed-off-by: JianMinTang Fixed alignment problem Signed-off-by: JianMinTang Fixed the code format Signed-off-by: JianMinTang * AutoGenerate credential name in login (#250) * generate credential name Signed-off-by: bupd * feat: add support for the password-stdin flag in login flow Signed-off-by: karanngi * fix deps - fixes dependencies Signed-off-by: bupd * return stdout for tests Signed-off-by: bupd * update workflow Signed-off-by: bupd --------- Signed-off-by: bupd Signed-off-by: karanngi Co-authored-by: karanngi Signed-off-by: JianMinTang * print test output to screen (#254) print test output to screen Signed-off-by: JianMinTang * Support yaml output for 'registry list' Signed-off-by: JianMinTang Use gofmt to format all code Signed-off-by: JianMinTang fix: Support YAML output for additional commands Signed-off-by: JianMinTang fix: Implement a generic function to format output Signed-off-by: JianMinTang chore: fix the problem about golangci-lint Signed-off-by: JianMinTang --------- Signed-off-by: JianMinTang Signed-off-by: bupd Signed-off-by: karanngi Co-authored-by: Prasanth B <89722848+bupd@users.noreply.github.com> Co-authored-by: karanngi Co-authored-by: Vadim Bauer Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/artifact/cmd.go | 2 +- cmd/harbor/root/artifact/info.go | 39 ------------------- cmd/harbor/root/artifact/list.go | 25 +++++++++---- cmd/harbor/root/artifact/tags.go | 31 +++++++++++---- cmd/harbor/root/artifact/view.go | 55 +++++++++++++++++++++++++++ cmd/harbor/root/project/list.go | 10 +++-- cmd/harbor/root/project/logs.go | 9 +++-- cmd/harbor/root/project/view.go | 25 +++++++++++-- cmd/harbor/root/registry/cmd.go | 2 +- cmd/harbor/root/registry/info.go | 34 ----------------- cmd/harbor/root/registry/list.go | 9 +++-- cmd/harbor/root/registry/view.go | 56 ++++++++++++++++++++++++++++ cmd/harbor/root/repository/cmd.go | 2 +- cmd/harbor/root/repository/info.go | 35 ----------------- cmd/harbor/root/repository/list.go | 31 ++++++++++----- cmd/harbor/root/repository/view.go | 54 +++++++++++++++++++++++++++ cmd/harbor/root/user/list.go | 5 ++- pkg/api/artifact_handler.go | 22 ++++++----- pkg/api/project_handler.go | 13 ++++--- pkg/api/registry_handler.go | 15 ++++---- pkg/api/repository_handler.go | 12 +++--- pkg/utils/helper.go | 12 ++++++ pkg/utils/utils.go | 14 +++++++ pkg/views/artifact/tags/list/view.go | 39 +++++++++++++++++++ pkg/views/artifact/view/view.go | 47 +++++++++++++++++++++++ pkg/views/project/view/view.go | 52 ++++++++++++++++++++++++++ pkg/views/registry/view/view.go | 43 +++++++++++++++++++++ pkg/views/repository/view/view.go | 47 +++++++++++++++++++++++ 28 files changed, 562 insertions(+), 178 deletions(-) delete mode 100644 cmd/harbor/root/artifact/info.go create mode 100644 cmd/harbor/root/artifact/view.go delete mode 100644 cmd/harbor/root/registry/info.go create mode 100644 cmd/harbor/root/registry/view.go delete mode 100644 cmd/harbor/root/repository/info.go create mode 100644 cmd/harbor/root/repository/view.go create mode 100644 pkg/views/artifact/tags/list/view.go create mode 100644 pkg/views/artifact/view/view.go create mode 100644 pkg/views/project/view/view.go create mode 100644 pkg/views/registry/view/view.go create mode 100644 pkg/views/repository/view/view.go diff --git a/cmd/harbor/root/artifact/cmd.go b/cmd/harbor/root/artifact/cmd.go index 9ae56ba1..8f9c66d2 100644 --- a/cmd/harbor/root/artifact/cmd.go +++ b/cmd/harbor/root/artifact/cmd.go @@ -15,7 +15,7 @@ func Artifact() *cobra.Command { cmd.AddCommand( ListArtifactCommand(), - InfoArtifactCommmand(), + ViewArtifactCommmand(), DeleteArtifactCommand(), ScanArtifactCommand(), ArtifactTagsCmd(), diff --git a/cmd/harbor/root/artifact/info.go b/cmd/harbor/root/artifact/info.go deleted file mode 100644 index b215d91a..00000000 --- a/cmd/harbor/root/artifact/info.go +++ /dev/null @@ -1,39 +0,0 @@ -package artifact - -import ( - "github.com/goharbor/harbor-cli/pkg/api" - "github.com/goharbor/harbor-cli/pkg/prompt" - "github.com/goharbor/harbor-cli/pkg/utils" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -func InfoArtifactCommmand() *cobra.Command { - - cmd := &cobra.Command{ - Use: "info", - Short: "Get info of an artifact", - Long: `Get info of an artifact`, - Example: `harbor artifact info //`, - Run: func(cmd *cobra.Command, args []string) { - var err error - - if len(args) > 0 { - projectName, repoName, reference := utils.ParseProjectRepoReference(args[0]) - err = api.InfoArtifact(projectName, repoName, reference) - } else { - projectName := prompt.GetProjectNameFromUser() - repoName := prompt.GetRepoNameFromUser(projectName) - reference := prompt.GetReferenceFromUser(repoName, projectName) - err = api.InfoArtifact(projectName, repoName, reference) - } - - if err != nil { - log.Errorf("failed to get info of an artifact: %v", err) - } - - }, - } - - return cmd -} diff --git a/cmd/harbor/root/artifact/list.go b/cmd/harbor/root/artifact/list.go index 5c6fb33f..c378f05e 100644 --- a/cmd/harbor/root/artifact/list.go +++ b/cmd/harbor/root/artifact/list.go @@ -8,6 +8,7 @@ import ( artifactViews "github.com/goharbor/harbor-cli/pkg/views/artifact/list" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" ) func ListArtifactCommand() *cobra.Command { @@ -19,21 +20,31 @@ func ListArtifactCommand() *cobra.Command { Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error - var resp artifact.ListArtifactsOK + var artifacts artifact.ListArtifactsOK + var projectName, repoName string if len(args) > 0 { - projectName, repoName := utils.ParseProjectRepo(args[0]) - resp, err = api.ListArtifact(projectName, repoName, opts) + projectName, repoName = utils.ParseProjectRepo(args[0]) } else { - projectName := prompt.GetProjectNameFromUser() - repoName := prompt.GetRepoNameFromUser(projectName) - resp, err = api.ListArtifact(projectName, repoName, opts) + projectName = prompt.GetProjectNameFromUser() + repoName = prompt.GetRepoNameFromUser(projectName) } + artifacts, err = api.ListArtifact(projectName, repoName, opts) + if err != nil { log.Errorf("failed to list artifacts: %v", err) } - artifactViews.ListArtifacts(resp.Payload) + + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + err = utils.PrintFormat(artifacts, FormatFlag) + if err != nil { + log.Error(err) + } + } else { + artifactViews.ListArtifacts(artifacts.Payload) + } }, } diff --git a/cmd/harbor/root/artifact/tags.go b/cmd/harbor/root/artifact/tags.go index f5b582d2..a213b3c7 100644 --- a/cmd/harbor/root/artifact/tags.go +++ b/cmd/harbor/root/artifact/tags.go @@ -6,8 +6,10 @@ import ( "github.com/goharbor/harbor-cli/pkg/prompt" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/goharbor/harbor-cli/pkg/views/artifact/tags/create" + "github.com/goharbor/harbor-cli/pkg/views/artifact/tags/list" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" ) func ArtifactTagsCmd() *cobra.Command { @@ -62,22 +64,35 @@ func ListTagsCmd() *cobra.Command { Example: `harbor artifact tags list //`, Run: func(cmd *cobra.Command, args []string) { var err error + var tags *artifact.ListTagsOK + var projectName, repoName, reference string - var resp artifact.ListTagsOK if len(args) > 0 { - projectName, repoName, reference := utils.ParseProjectRepoReference(args[0]) - resp, err = api.ListTags(projectName, repoName, reference) + projectName, repoName, reference = utils.ParseProjectRepoReference(args[0]) } else { - projectName := prompt.GetProjectNameFromUser() - repoName := prompt.GetRepoNameFromUser(projectName) - reference := prompt.GetReferenceFromUser(repoName, projectName) - resp, err = api.ListTags(projectName, repoName, reference) + projectName = prompt.GetProjectNameFromUser() + repoName = prompt.GetRepoNameFromUser(projectName) + reference = prompt.GetReferenceFromUser(repoName, projectName) } + + tags, err = api.ListTags(projectName, repoName, reference) + if err != nil { log.Errorf("failed to list tags: %v", err) + return + } + + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + err = utils.PrintFormat(tags, FormatFlag) + if err != nil { + log.Error(err) + return + } + } else { + list.ListTags(tags.Payload) } - log.Info(resp.Payload) }, } diff --git a/cmd/harbor/root/artifact/view.go b/cmd/harbor/root/artifact/view.go new file mode 100644 index 00000000..7d13c0f6 --- /dev/null +++ b/cmd/harbor/root/artifact/view.go @@ -0,0 +1,55 @@ +package artifact + +import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/artifact" + "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/artifact/view" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func ViewArtifactCommmand() *cobra.Command { + + cmd := &cobra.Command{ + Use: "view", + Short: "Get information of an artifact", + Long: `Get information of an artifact`, + Example: `harbor artifact view //`, + Run: func(cmd *cobra.Command, args []string) { + var err error + var projectName, repoName, reference string + var artifact *artifact.GetArtifactOK + + if len(args) > 0 { + projectName, repoName, reference = utils.ParseProjectRepoReference(args[0]) + } else { + projectName = prompt.GetProjectNameFromUser() + repoName = prompt.GetRepoNameFromUser(projectName) + reference = prompt.GetReferenceFromUser(repoName, projectName) + } + + artifact, err = api.ViewArtifact(projectName, repoName, reference) + + if err != nil { + log.Errorf("failed to get info of an artifact: %v", err) + } + + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + err = utils.PrintFormat(artifact, FormatFlag) + if err != nil { + log.Error(err) + return + } + } else { + view.ViewArtifact(artifact.Payload) + } + + }, + } + + return cmd +} diff --git a/cmd/harbor/root/project/list.go b/cmd/harbor/root/project/list.go index 71f18de2..3a908b42 100644 --- a/cmd/harbor/root/project/list.go +++ b/cmd/harbor/root/project/list.go @@ -37,11 +37,13 @@ func ListProjectCommand() *cobra.Command { } FormatFlag := viper.GetString("output-format") if FormatFlag != "" { - utils.PrintPayloadInJSONFormat(projects) - return + err = utils.PrintFormat(projects, FormatFlag) + if err != nil { + log.Error(err) + } + } else { + list.ListProjects(projects.Payload) } - - list.ListProjects(projects.Payload) }, } diff --git a/cmd/harbor/root/project/logs.go b/cmd/harbor/root/project/logs.go index 6ee330db..8e4cac54 100644 --- a/cmd/harbor/root/project/logs.go +++ b/cmd/harbor/root/project/logs.go @@ -30,12 +30,15 @@ func LogsProjectCommmand() *cobra.Command { if err != nil { log.Fatalf("failed to get project logs: %v", err) } - auditLog.LogsProject(resp.Payload) FormatFlag := viper.GetString("output-format") if FormatFlag != "" { - utils.PrintPayloadInJSONFormat(resp) - return + err = utils.PrintFormat(resp, FormatFlag) + if err != nil { + log.Error(err) + } + } else { + auditLog.LogsProject(resp.Payload) } }, diff --git a/cmd/harbor/root/project/view.go b/cmd/harbor/root/project/view.go index 908d51d8..7d7addc4 100644 --- a/cmd/harbor/root/project/view.go +++ b/cmd/harbor/root/project/view.go @@ -1,10 +1,14 @@ package project import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/project" "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/project/view" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // GetProjectCommand creates a new `harbor get project` command @@ -16,16 +20,31 @@ func ViewCommand() *cobra.Command { Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error + var projectName string + var project *project.GetProjectOK if len(args) > 0 { - err = api.GetProject(args[0]) + projectName = args[0] } else { - projectName := prompt.GetProjectNameFromUser() - err = api.GetProject(projectName) + projectName = prompt.GetProjectNameFromUser() } + project, err = api.GetProject(projectName) + if err != nil { log.Errorf("failed to get project: %v", err) + return + } + + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + err = utils.PrintFormat(project, FormatFlag) + if err != nil { + log.Error(err) + return + } + } else { + view.ViewProjects(project.Payload) } }, diff --git a/cmd/harbor/root/registry/cmd.go b/cmd/harbor/root/registry/cmd.go index 9f42d761..3f9d2e26 100644 --- a/cmd/harbor/root/registry/cmd.go +++ b/cmd/harbor/root/registry/cmd.go @@ -13,7 +13,7 @@ func Registry() *cobra.Command { } cmd.AddCommand( CreateRegistryCommand(), - InfoRegistryCommand(), + ViewRegistryCommand(), DeleteRegistryCommand(), ListRegistryCommand(), UpdateRegistryCommand(), diff --git a/cmd/harbor/root/registry/info.go b/cmd/harbor/root/registry/info.go deleted file mode 100644 index 97a16799..00000000 --- a/cmd/harbor/root/registry/info.go +++ /dev/null @@ -1,34 +0,0 @@ -package registry - -import ( - "github.com/goharbor/harbor-cli/pkg/api" - "github.com/goharbor/harbor-cli/pkg/prompt" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -func InfoRegistryCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "info", - Short: "get registry info", - Example: "harbor registry info [registryname]", - Args: cobra.MaximumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - var err error - - if len(args) > 0 { - registryName, _ := api.GetRegistryIdByName(args[0]) - err = api.InfoRegistry(registryName) - } else { - registryId := prompt.GetRegistryNameFromUser() - err = api.InfoRegistry(registryId) - } - if err != nil { - log.Errorf("failed to get registry info: %v", err) - } - - }, - } - - return cmd -} diff --git a/cmd/harbor/root/registry/list.go b/cmd/harbor/root/registry/list.go index 21862779..78598511 100644 --- a/cmd/harbor/root/registry/list.go +++ b/cmd/harbor/root/registry/list.go @@ -24,10 +24,13 @@ func ListRegistryCommand() *cobra.Command { } FormatFlag := viper.GetString("output-format") if FormatFlag != "" { - utils.PrintPayloadInJSONFormat(registry) - return + err = utils.PrintFormat(registry, FormatFlag) + if err != nil { + log.Error(err) + } + } else { + list.ListRegistry(registry.Payload) } - list.ListRegistry(registry.Payload) }, } diff --git a/cmd/harbor/root/registry/view.go b/cmd/harbor/root/registry/view.go new file mode 100644 index 00000000..f7910971 --- /dev/null +++ b/cmd/harbor/root/registry/view.go @@ -0,0 +1,56 @@ +package registry + +import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/registry" + "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/registry/view" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func ViewRegistryCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "view", + Short: "get registry information", + Example: "harbor registry view [registryName]", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + var registryId int64 + var registry *registry.GetRegistryOK + + if len(args) > 0 { + registryId, err = api.GetRegistryIdByName(args[0]) + if err != nil { + log.Errorf("failed to get registry name by id: %v", err) + return + } + } else { + registryId = prompt.GetRegistryNameFromUser() + } + + registry, err = api.ViewRegistry(registryId) + + if err != nil { + log.Errorf("failed to get registry info: %v", err) + return + } + + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + err = utils.PrintFormat(registry, FormatFlag) + if err != nil { + log.Error(err) + } + } else { + view.ViewRegistry(registry.Payload) + } + + }, + } + + return cmd +} diff --git a/cmd/harbor/root/repository/cmd.go b/cmd/harbor/root/repository/cmd.go index f5144078..c9da6a63 100644 --- a/cmd/harbor/root/repository/cmd.go +++ b/cmd/harbor/root/repository/cmd.go @@ -10,7 +10,7 @@ func Repository() *cobra.Command { } cmd.AddCommand( ListRepositoryCommand(), - RepoInfoCmd(), + RepoViewCmd(), RepoDeleteCmd(), SearchRepoCmd(), ) diff --git a/cmd/harbor/root/repository/info.go b/cmd/harbor/root/repository/info.go deleted file mode 100644 index 6a54da71..00000000 --- a/cmd/harbor/root/repository/info.go +++ /dev/null @@ -1,35 +0,0 @@ -package repository - -import ( - "github.com/goharbor/harbor-cli/pkg/api" - "github.com/goharbor/harbor-cli/pkg/prompt" - "github.com/goharbor/harbor-cli/pkg/utils" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -func RepoInfoCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "info", - Short: "Get repository information", - Example: ` harbor repo info /`, - Long: `Get information of a particular repository in a project`, - Run: func(cmd *cobra.Command, args []string) { - var err error - if len(args) > 0 { - projectName, repoName := utils.ParseProjectRepo(args[0]) - err = api.RepoInfo(projectName, repoName) - } else { - projectName := prompt.GetProjectNameFromUser() - repoName := prompt.GetRepoNameFromUser(projectName) - err = api.RepoInfo(projectName, repoName) - } - if err != nil { - log.Errorf("failed to get repository information: %v", err) - } - - }, - } - - return cmd -} diff --git a/cmd/harbor/root/repository/list.go b/cmd/harbor/root/repository/list.go index e9f1126c..41ca0492 100644 --- a/cmd/harbor/root/repository/list.go +++ b/cmd/harbor/root/repository/list.go @@ -4,33 +4,46 @@ import ( "github.com/goharbor/go-client/pkg/sdk/v2.0/client/repository" "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/repository/list" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" ) func ListRepositoryCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "list", - Short: "list repositories within a project", - Args: cobra.MaximumNArgs(1), + Use: "list", + Short: "list repositories within a project", + Example: ` harbor repo list `, + Long: `Get information of all repositories in a project`, + Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error - var resp repository.ListRepositoriesOK + var repos repository.ListRepositoriesOK + var projectName string if len(args) > 0 { - resp, err = api.ListRepository(args[0]) + projectName = args[0] } else { - projectName := prompt.GetProjectNameFromUser() - resp, err = api.ListRepository(projectName) + projectName = prompt.GetProjectNameFromUser() } + repos, err = api.ListRepository(projectName) + if err != nil { log.Errorf("failed to list repositories: %v", err) } - list.ListRepositories(resp.Payload) - + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + err = utils.PrintFormat(repos, FormatFlag) + if err != nil { + log.Error(err) + } + } else { + list.ListRepositories(repos.Payload) + } }, } diff --git a/cmd/harbor/root/repository/view.go b/cmd/harbor/root/repository/view.go new file mode 100644 index 00000000..463387df --- /dev/null +++ b/cmd/harbor/root/repository/view.go @@ -0,0 +1,54 @@ +package repository + +import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/repository" + "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/repository/view" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func RepoViewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "view", + Short: "Get repository information", + Example: ` harbor repo view /`, + Long: `Get information of a particular repository in a project`, + Run: func(cmd *cobra.Command, args []string) { + var err error + var projectName, repoName string + var repo *repository.GetRepositoryOK + + if len(args) > 0 { + projectName, repoName = utils.ParseProjectRepo(args[0]) + } else { + projectName = prompt.GetProjectNameFromUser() + repoName = prompt.GetRepoNameFromUser(projectName) + } + + repo, err = api.RepoView(projectName, repoName) + + if err != nil { + log.Errorf("failed to get repository information: %v", err) + return + } + + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + err = utils.PrintFormat(repo, FormatFlag) + if err != nil { + log.Error(err) + return + } + } else { + view.ViewRepository(repo.Payload) + } + + }, + } + + return cmd +} diff --git a/cmd/harbor/root/user/list.go b/cmd/harbor/root/user/list.go index d8a81aab..9a7999e9 100644 --- a/cmd/harbor/root/user/list.go +++ b/cmd/harbor/root/user/list.go @@ -25,7 +25,10 @@ func UserListCmd() *cobra.Command { } FormatFlag := viper.GetString("output-format") if FormatFlag != "" { - utils.PrintPayloadInJSONFormat(response.Payload) + err = utils.PrintFormat(response, FormatFlag) + if err != nil { + log.Error(err) + } } else { list.ListUsers(response.Payload) } diff --git a/pkg/api/artifact_handler.go b/pkg/api/artifact_handler.go index a6fa1391..d79eba44 100644 --- a/pkg/api/artifact_handler.go +++ b/pkg/api/artifact_handler.go @@ -30,24 +30,25 @@ func DeleteArtifact(projectName, repoName, reference string) error { } // InfoArtifact retrieves information about a specific artifact. -func InfoArtifact(projectName, repoName, reference string) error { +func ViewArtifact(projectName, repoName, reference string) (*artifact.GetArtifactOK, error) { ctx, client, err := utils.ContextWithClient() + var response = &artifact.GetArtifactOK{} if err != nil { - return err + return response, err } - response, err := client.Artifact.GetArtifact(ctx, &artifact.GetArtifactParams{ + response, err = client.Artifact.GetArtifact(ctx, &artifact.GetArtifactParams{ ProjectName: projectName, RepositoryName: repoName, Reference: reference, }) + if err != nil { log.Errorf("Failed to get artifact info: %v", err) - return err + return response, err } - utils.PrintPayloadInJSONFormat(response.Payload) - return nil + return response, nil } // RunListArtifact lists all artifacts in a repository. @@ -141,10 +142,10 @@ func DeleteTag(projectName, repoName, reference, tag string) error { } // ListTags lists all tags of a specific artifact. -func ListTags(projectName, repoName, reference string) (artifact.ListTagsOK, error) { +func ListTags(projectName, repoName, reference string) (*artifact.ListTagsOK, error) { ctx, client, err := utils.ContextWithClient() if err != nil { - return artifact.ListTagsOK{}, err + return &artifact.ListTagsOK{}, err } resp, err := client.Artifact.ListTags(ctx, &artifact.ListTagsParams{ @@ -152,12 +153,13 @@ func ListTags(projectName, repoName, reference string) (artifact.ListTagsOK, err RepositoryName: repoName, Reference: reference, }) + if err != nil { log.Errorf("Failed to list tags: %v", err) - return artifact.ListTagsOK{}, err + return &artifact.ListTagsOK{}, err } - return *resp, nil + return resp, nil } // CreateTag creates a tag for a specific artifact. diff --git a/pkg/api/project_handler.go b/pkg/api/project_handler.go index 5a06e9da..9b19a468 100644 --- a/pkg/api/project_handler.go +++ b/pkg/api/project_handler.go @@ -40,20 +40,21 @@ func CreateProject(opts create.CreateView) error { return nil } -func GetProject(projectName string) error { +func GetProject(projectName string) (*project.GetProjectOK, error) { ctx, client, err := utils.ContextWithClient() + var response = &project.GetProjectOK{} + if err != nil { - return err + return response, err } - response, err := client.Project.GetProject(ctx, &project.GetProjectParams{ProjectNameOrID: projectName}) + response, err = client.Project.GetProject(ctx, &project.GetProjectParams{ProjectNameOrID: projectName}) if err != nil { - return err + return response, err } - utils.PrintPayloadInJSONFormat(response) - return nil + return response, nil } func DeleteProject(projectName string, forceDelete bool) error { diff --git a/pkg/api/registry_handler.go b/pkg/api/registry_handler.go index e3a341cb..0df427fc 100644 --- a/pkg/api/registry_handler.go +++ b/pkg/api/registry_handler.go @@ -81,22 +81,23 @@ func DeleteRegistry(registryName int64) error { return nil } -func InfoRegistry(registryId int64) error { +func ViewRegistry(registryId int64) (*registry.GetRegistryOK, error) { ctx, client, err := utils.ContextWithClient() + var response = ®istry.GetRegistryOK{} if err != nil { - return err + return response, err } - response, err := client.Registry.GetRegistry(ctx, ®istry.GetRegistryParams{ID: registryId}) + response, err = client.Registry.GetRegistry(ctx, ®istry.GetRegistryParams{ID: registryId}) + if err != nil { - return err + return response, err } if response.Payload.ID == 0 { - return fmt.Errorf("registry is not found") + return response, fmt.Errorf("registry is not found") } - utils.PrintPayloadInJSONFormat(response.Payload) - return nil + return response, nil } func UpdateRegistry(updateView *CreateRegView, projectID int64) error { diff --git a/pkg/api/repository_handler.go b/pkg/api/repository_handler.go index 187c174c..479fdd1f 100644 --- a/pkg/api/repository_handler.go +++ b/pkg/api/repository_handler.go @@ -22,20 +22,20 @@ func RepoDelete(projectName, repoName string) error { return nil } -func RepoInfo(projectName, repoName string) error { +func RepoView(projectName, repoName string) (*repository.GetRepositoryOK, error) { ctx, client, err := utils.ContextWithClient() + var response = &repository.GetRepositoryOK{} if err != nil { - return err + return response, err } - response, err := client.Repository.GetRepository(ctx, &repository.GetRepositoryParams{ProjectName: projectName, RepositoryName: repoName}) + response, err = client.Repository.GetRepository(ctx, &repository.GetRepositoryParams{ProjectName: projectName, RepositoryName: repoName}) if err != nil { - return err + return response, err } - utils.PrintPayloadInJSONFormat(response.Payload) - return nil + return response, nil } func ListRepository(projectName string) (repository.ListRepositoriesOK, error) { diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index 6421243b..22863f5e 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -128,3 +128,15 @@ func ValidateRegistryName(rn string) bool { return re.MatchString(rn) } + +func PrintFormat[T any](resp T, format string) error { + if format == "json" { + PrintPayloadInJSONFormat(resp) + return nil + } + if format == "yaml" { + PrintPayloadInYAMLFormat(resp) + return nil + } + return fmt.Errorf("unable to output in the specified '%s' format", format) +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index db99de76..5bff7837 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -7,6 +7,7 @@ import ( "strings" log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" ) // Returns Harbor v2 client for given clientConfig @@ -24,6 +25,19 @@ func PrintPayloadInJSONFormat(payload any) { fmt.Println(string(jsonStr)) } +func PrintPayloadInYAMLFormat(payload any) { + if payload == nil { + return + } + + yamlStr, err := yaml.Marshal(payload) + if err != nil { + panic(err) + } + + fmt.Println(string(yamlStr)) +} + func ParseProjectRepo(projectRepo string) (string, string) { split := strings.Split(projectRepo, "/") if len(split) != 2 { diff --git a/pkg/views/artifact/tags/list/view.go b/pkg/views/artifact/tags/list/view.go new file mode 100644 index 00000000..f20491b5 --- /dev/null +++ b/pkg/views/artifact/tags/list/view.go @@ -0,0 +1,39 @@ +package list + +import ( + "fmt" + "os" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "Name", Width: 12}, + {Title: "Pull Time", Width: 30}, + {Title: "Push Time", Width: 30}, +} + +func ListTags(tags []*models.Tag) { + var rows []table.Row + for _, tag := range tags { + + pullTime, _ := utils.FormatCreatedTime(tag.PullTime.String()) + pushTime, _ := utils.FormatCreatedTime(tag.PushTime.String()) + rows = append(rows, table.Row{ + tag.Name, + pullTime, + pushTime, + }) + } + + m := tablelist.NewModel(columns, rows, len(rows)) + + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/pkg/views/artifact/view/view.go b/pkg/views/artifact/view/view.go new file mode 100644 index 00000000..0ab46ae7 --- /dev/null +++ b/pkg/views/artifact/view/view.go @@ -0,0 +1,47 @@ +package view + +import ( + "fmt" + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" + "os" + "strconv" +) + +var columns = []table.Column{ + {Title: "ID", Width: 6}, + {Title: "Artifact Digest", Width: 20}, + {Title: "Type", Width: 12}, + {Title: "Size", Width: 12}, + {Title: "Vulnerabilities", Width: 15}, + {Title: "Push Time", Width: 12}, +} + +func ViewArtifact(artifact *models.Artifact) { + var rows []table.Row + + pushTime, _ := utils.FormatCreatedTime(artifact.PushTime.String()) + artifactSize := utils.FormatSize(artifact.Size) + var totalVulnerabilities int64 + for _, scan := range artifact.ScanOverview { + totalVulnerabilities += scan.Summary.Total + } + rows = append(rows, table.Row{ + strconv.FormatInt(int64(artifact.ID), 10), + artifact.Digest[:16], + artifact.Type, + artifactSize, + strconv.FormatInt(totalVulnerabilities, 10), + pushTime, + }) + + m := tablelist.NewModel(columns, rows, len(rows)) + + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/pkg/views/project/view/view.go b/pkg/views/project/view/view.go new file mode 100644 index 00000000..66e2b937 --- /dev/null +++ b/pkg/views/project/view/view.go @@ -0,0 +1,52 @@ +package view + +import ( + "fmt" + "os" + "strconv" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "Project ID", Width: 10}, + {Title: "Project Name", Width: 12}, + {Title: "Access Level", Width: 12}, + {Title: "Type", Width: 12}, + {Title: "Repo Count", Width: 12}, + {Title: "Creation Time", Width: 15}, +} + +func ViewProjects(project *models.Project) { + var rows []table.Row + accessLevel := "public" + if project.Metadata.Public != "true" { + accessLevel = "private" + } + + projectType := "project" + + if project.RegistryID != 0 { + projectType = "proxy cache" + } + createdTime, _ := utils.FormatCreatedTime(project.CreationTime.String()) + rows = append(rows, table.Row{ + strconv.FormatInt(int64(project.ProjectID), 10), // ProjectID + project.Name, // Project Name + accessLevel, // Access Level + projectType, // Type + strconv.FormatInt(project.RepoCount, 10), + createdTime, // Creation Time, + }) + + m := tablelist.NewModel(columns, rows, len(rows)) + + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/pkg/views/registry/view/view.go b/pkg/views/registry/view/view.go new file mode 100644 index 00000000..f792b930 --- /dev/null +++ b/pkg/views/registry/view/view.go @@ -0,0 +1,43 @@ +package view + +import ( + "fmt" + "os" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "ID", Width: 5}, + {Title: "Name", Width: 10}, + {Title: "Status", Width: 10}, + {Title: "Endpoint URL", Width: 20}, + {Title: "Provider", Width: 15}, + {Title: "Creation Time", Width: 15}, + {Title: "Description", Width: 20}, +} + +func ViewRegistry(registry *models.Registry) { + var rows []table.Row + createdTime, _ := utils.FormatCreatedTime(registry.CreationTime.String()) + rows = append(rows, table.Row{ + fmt.Sprintf("%d", registry.ID), + registry.Name, + registry.Status, + registry.URL, + registry.Type, + createdTime, + registry.Description, + }) + + m := tablelist.NewModel(columns, rows, len(rows)) + + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/pkg/views/repository/view/view.go b/pkg/views/repository/view/view.go new file mode 100644 index 00000000..0b5944b5 --- /dev/null +++ b/pkg/views/repository/view/view.go @@ -0,0 +1,47 @@ +package view + +import ( + "fmt" + "os" + "strconv" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "Name", Width: 20}, + {Title: "ID", Width: 10}, + {Title: "Project ID", Width: 10}, + {Title: "Artifacts", Width: 10}, + {Title: "Pulls", Width: 5}, + {Title: "Creation Time", Width: 20}, + {Title: "Last Modified Time", Width: 20}, + {Title: "Description", Width: 20}, +} + +func ViewRepository(repo *models.Repository) { + var rows []table.Row + + createdTime, _ := utils.FormatCreatedTime(repo.CreationTime.String()) + modifledTime, _ := utils.FormatCreatedTime(repo.UpdateTime.String()) + rows = append(rows, table.Row{ + repo.Name, + fmt.Sprintf("%d", repo.ID), + fmt.Sprintf("%d", repo.ProjectID), + fmt.Sprintf("%d", repo.ArtifactCount), + strconv.FormatInt(repo.PullCount, 10), + createdTime, + modifledTime, + repo.Description, + }) + + 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 d86747849e166e2a0c0e2f8943c65a789c1bfbc2 Mon Sep 17 00:00:00 2001 From: ALTHAF <114910365+Althaf66@users.noreply.github.com> Date: Tue, 26 Nov 2024 20:25:42 +0530 Subject: [PATCH 13/41] Add: created schedule cmd (#121) * created schedule cmd Signed-off-by: ALTHAF * Update cmd.go Signed-off-by: ALTHAF <114910365+Althaf66@users.noreply.github.com> * Update cmd.go Signed-off-by: ALTHAF <114910365+Althaf66@users.noreply.github.com> * fix lint error Signed-off-by: ALTHAF --------- Signed-off-by: ALTHAF Signed-off-by: ALTHAF <114910365+Althaf66@users.noreply.github.com> Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/cmd.go | 2 ++ cmd/harbor/root/schedule/cmd.go | 17 ++++++++++++++ cmd/harbor/root/schedule/list.go | 38 +++++++++++++++++++++++++++++++ pkg/api/schedule_handler.go | 30 ++++++++++++++++++++++++ pkg/views/schedule/list/view.go | 39 ++++++++++++++++++++++++++++++++ 5 files changed, 126 insertions(+) create mode 100644 cmd/harbor/root/schedule/cmd.go create mode 100644 cmd/harbor/root/schedule/list.go create mode 100644 pkg/api/schedule_handler.go create mode 100644 pkg/views/schedule/list/view.go diff --git a/cmd/harbor/root/cmd.go b/cmd/harbor/root/cmd.go index a975dbeb..8ff6bb73 100644 --- a/cmd/harbor/root/cmd.go +++ b/cmd/harbor/root/cmd.go @@ -7,6 +7,7 @@ import ( "github.com/goharbor/harbor-cli/cmd/harbor/root/project" "github.com/goharbor/harbor-cli/cmd/harbor/root/registry" repositry "github.com/goharbor/harbor-cli/cmd/harbor/root/repository" + "github.com/goharbor/harbor-cli/cmd/harbor/root/schedule" "github.com/goharbor/harbor-cli/cmd/harbor/root/user" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/spf13/cobra" @@ -65,6 +66,7 @@ harbor help user.User(), artifact.Artifact(), HealthCommand(), + schedule.Schedule(), ) return root diff --git a/cmd/harbor/root/schedule/cmd.go b/cmd/harbor/root/schedule/cmd.go new file mode 100644 index 00000000..8b2a98d1 --- /dev/null +++ b/cmd/harbor/root/schedule/cmd.go @@ -0,0 +1,17 @@ +package schedule + +import ( + "github.com/spf13/cobra" +) + +func Schedule() *cobra.Command { + cmd := &cobra.Command{ + Use: "schedule", + Short: "Schedule jobs in Harbor", + } + cmd.AddCommand( + ListScheduleCommand(), + ) + + return cmd +} diff --git a/cmd/harbor/root/schedule/list.go b/cmd/harbor/root/schedule/list.go new file mode 100644 index 00000000..91522054 --- /dev/null +++ b/cmd/harbor/root/schedule/list.go @@ -0,0 +1,38 @@ +package schedule + +import ( + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/schedule/list" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func ListScheduleCommand() *cobra.Command { + var opts api.ListFlags + + cmd := &cobra.Command{ + Use: "list", + Short: "show all schedule jobs in Harbor", + Run: func(cmd *cobra.Command, args []string) { + schedule, err := api.ListSchedule(opts) + if err != nil { + log.Fatalf("failed to get schedule list: %v", err) + } + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + utils.PrintPayloadInJSONFormat(schedule) + return + } + + list.ListSchedule(schedule.Payload) + }, + } + + flags := cmd.Flags() + flags.Int64VarP(&opts.Page, "page", "", 1, "Page number") + flags.Int64VarP(&opts.PageSize, "page-size", "", 10, "Size of per page") + + return cmd +} diff --git a/pkg/api/schedule_handler.go b/pkg/api/schedule_handler.go new file mode 100644 index 00000000..b58fe1e9 --- /dev/null +++ b/pkg/api/schedule_handler.go @@ -0,0 +1,30 @@ +package api + +import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/schedule" + "github.com/goharbor/harbor-cli/pkg/utils" +) + +func ListSchedule(opts ...ListFlags) (schedule.ListSchedulesOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return schedule.ListSchedulesOK{}, err + } + + var listFlags ListFlags + if len(opts) > 0 { + listFlags = opts[0] + } + + response, err := client.Schedule.ListSchedules(ctx, &schedule.ListSchedulesParams{ + Page: &listFlags.Page, + PageSize: &listFlags.PageSize, + }) + + if err != nil { + return schedule.ListSchedulesOK{}, err + } + + return *response, nil + +} diff --git a/pkg/views/schedule/list/view.go b/pkg/views/schedule/list/view.go new file mode 100644 index 00000000..ffb55122 --- /dev/null +++ b/pkg/views/schedule/list/view.go @@ -0,0 +1,39 @@ +package list + +import ( + "fmt" + "os" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "ID", Width: 6}, + {Title: "Cron", Width: 18}, + {Title: "Vendor Type", Width: 28}, + {Title: "Update Time", Width: 20}, +} + +func ListSchedule(schedule []*models.ScheduleTask) { + var rows []table.Row + for _, regis := range schedule { + updatedTime, _ := utils.FormatCreatedTime(regis.UpdateTime.String()) + rows = append(rows, table.Row{ + fmt.Sprintf("%d", regis.ID), + regis.Cron, + regis.VendorType, + updatedTime, + }) + } + + 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 c16640d332f204a66db95671d05e6dbbab91c5e5 Mon Sep 17 00:00:00 2001 From: Prasanth B <89722848+bupd@users.noreply.github.com> Date: Tue, 26 Nov 2024 20:33:26 +0530 Subject: [PATCH 14/41] fix-username validation (#265) Signed-off-by: bupd Signed-off-by: Patrick Eschenbach --- pkg/utils/helper.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index 22863f5e..ba11029e 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -44,10 +44,10 @@ func FormatSize(size int64) string { return fmt.Sprintf("%.2fMiB", mbSize) } +// ValidateUserName checks if the username is valid by length and allowed characters. func ValidateUserName(username string) bool { - pattern := `^[a-zA-Z0-9]{1,255}$` - re := regexp.MustCompile(pattern) - return re.MatchString(username) + username = strings.TrimSpace(username) + return len(username) >= 1 && len(username) <= 255 && !strings.ContainsAny(username, `,"~#%$`) } func ValidateEmail(email string) bool { From 8837cd0f11ea3280d44105e1f9571fcab9823348 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Sat, 30 Nov 2024 19:59:15 +0100 Subject: [PATCH 15/41] Added check for existing credentials in present config for login; Skip login view creation if some credentials exist Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/login.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cmd/harbor/root/login.go b/cmd/harbor/root/login.go index 78750fbe..8c02699a 100644 --- a/cmd/harbor/root/login.go +++ b/cmd/harbor/root/login.go @@ -56,10 +56,30 @@ func LoginCommand() *cobra.Command { loginView.Name = fmt.Sprintf("%s@%s", loginView.Username, utils.SanitizeServerAddress(loginView.Server)) } + // Check whether there is already a config with credentials var err error + var config *utils.HarborConfig + var creds []utils.Credential + config, err = utils.GetCurrentHarborConfig() + if err != nil { + return fmt.Errorf("failed to get current harbor config: %s", err) + } + currentCredentialName := config.CurrentCredentialName if loginView.Server != "" && loginView.Username != "" && loginView.Password != "" { err = runLogin(loginView) + } else if currentCredentialName != "" { + var resolvedLoginView login.LoginView + creds = config.Credentials + for _, cred := range creds { + resolvedLoginView = login.LoginView{ + Server: cred.ServerAddress, + Username: cred.Username, + Password: cred.Password, + Name: cred.Name, + } + } + err = runLogin(resolvedLoginView) } else { err = createLoginView(&loginView) } From fb877e784518d42d5c5d572265fc4cb339339c13 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Sun, 1 Dec 2024 10:44:37 +0100 Subject: [PATCH 16/41] Added config and data path management description to readme Signed-off-by: Patrick Eschenbach --- README.md | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d01e8a57..6bf945ea 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Available Commands: version Version of Harbor CLI Flags: - --config string config file (default is $HOME/.harbor/config.yaml) (default "/Users/vadim/.harbor/config.yaml") + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) -h, --help help for harbor -o, --output-format string Output format. One of: json|yaml -v, --verbose verbose output @@ -100,8 +100,32 @@ Use "harbor [command] --help" for more information about a command. ``` - - +#### Config Management + +##### Hierachy + Use the `--config` flag to specify a custom configuration file path (highest priority). + ```bash + harbor --config /path/to/custom/config.yaml artifact list + ``` + If `--config` is not provided, Harbor CLI checks the `HARBOR_CLI_CONFIG` environment variable for the config file path. + ```bash + export HARBOR_CLI_CONFIG=/path/to/custom/config.yaml + harbor artifact list + ``` + If neither is set, it defaults to `$XDG_CONFIG_HOME/harbor-cli/config.yaml` or `$HOME/.config/harbor-cli/config.yaml` if `XDG_CONFIG_HOME` is unset. + ```bash + harbor artifact list + ``` + +##### Data Path + - Data paths are determined by the `XDG_DATA_HOME` environment variable. + - If `XDG_DATA_HOME` is not set, it defaults to `$HOME/.local/share/harbor-cli/data.yaml`. + - The data file always contains the path of the latest config used. + +##### Config TL;DR + - `--config` flag > `HARBOR_CLI_CONFIG` environment variable > default XDG config paths. + - Environment variables override default settings, and the `--config` flag takes precedence over both environment variables and defaults. + - The data file always contains the path of the latest config used. #### Log in to Harbor Registry From 0f5115ef5fcee8a21ff668e1dd440c32c32341ec Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Sun, 1 Dec 2024 11:05:56 +0100 Subject: [PATCH 17/41] Added harbor-config docs; Add ed config behavior to login docs Signed-off-by: Patrick Eschenbach --- doc/cli-docs/harbor-artifact-view.md | 42 ++++++++++++++++++++++ doc/cli-docs/harbor-config.md | 50 +++++++++++++++++++++++++++ doc/cli-docs/harbor-login.md | 20 +++++++++++ doc/cli-docs/harbor-project-search.md | 32 +++++++++++++++++ doc/cli-docs/harbor-repo-search.md | 32 +++++++++++++++++ doc/cli-docs/harbor-repo-view.md | 42 ++++++++++++++++++++++ doc/cli-docs/harbor-schedule-list.md | 34 ++++++++++++++++++ doc/cli-docs/harbor-schedule.md | 29 ++++++++++++++++ doc/cli-docs/harbor.md | 2 +- 9 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 doc/cli-docs/harbor-artifact-view.md create mode 100644 doc/cli-docs/harbor-config.md create mode 100644 doc/cli-docs/harbor-project-search.md create mode 100644 doc/cli-docs/harbor-repo-search.md create mode 100644 doc/cli-docs/harbor-repo-view.md create mode 100644 doc/cli-docs/harbor-schedule-list.md create mode 100644 doc/cli-docs/harbor-schedule.md diff --git a/doc/cli-docs/harbor-artifact-view.md b/doc/cli-docs/harbor-artifact-view.md new file mode 100644 index 00000000..0c7a12f9 --- /dev/null +++ b/doc/cli-docs/harbor-artifact-view.md @@ -0,0 +1,42 @@ +--- +title: harbor artifact view +weight: 75 +--- +## harbor artifact view + +### Description + +##### Get information of an artifact + +### Synopsis + +Get information of an artifact + +```sh +harbor artifact view [flags] +``` + +### Examples + +```sh +harbor artifact view // +``` + +### Options + +```sh + -h, --help help for view +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor artifact](harbor-artifact.md) - Manage artifacts + diff --git a/doc/cli-docs/harbor-config.md b/doc/cli-docs/harbor-config.md new file mode 100644 index 00000000..c96eeab7 --- /dev/null +++ b/doc/cli-docs/harbor-config.md @@ -0,0 +1,50 @@ +--- +title: harbor config +weight: 15 +--- +## harbor config + +### Description + +##### Manage the config of the Harbor cli + +### Synopsis + +#### Config +Use the `--config` flag to specify a custom configuration file path (highest priority). +```bash +harbor --config /path/to/custom/config.yaml artifact list +``` +If `--config` is not provided, Harbor CLI checks the `HARBOR_CLI_CONFIG` environment variable for the config file path. +```bash +export HARBOR_CLI_CONFIG=/path/to/custom/config.yaml +harbor artifact list +``` +If neither is set, it defaults to `$XDG_CONFIG_HOME/harbor-cli/config.yaml` or `$HOME/.config/harbor-cli/config.yaml` if `XDG_CONFIG_HOME` is unset. +```bash +harbor artifact list +``` + +#### Data + - Data paths are determined by the `XDG_DATA_HOME` environment variable. + - If `XDG_DATA_HOME` is not set, it defaults to `$HOME/.local/share/harbor-cli/data.yaml`. + - The data file always contains the path of the latest config used. + +### Options + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -h, --help help for harbor + -o, --output-format string Output format. One of: json|yaml + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor artifact](harbor-artifact.md) - Manage artifacts +* [harbor login](harbor-login.md) - Log in to Harbor registry +* [harbor project](harbor-project.md) - Manage projects and assign resources to them +* [harbor registry](harbor-registry.md) - Manage registries +* [harbor repo](harbor-repo.md) - Manage repositories +* [harbor user](harbor-user.md) - Manage users +* [harbor version](harbor-version.md) - Version of Harbor CLI diff --git a/doc/cli-docs/harbor-login.md b/doc/cli-docs/harbor-login.md index 4e401829..3e428115 100644 --- a/doc/cli-docs/harbor-login.md +++ b/doc/cli-docs/harbor-login.md @@ -8,6 +8,25 @@ weight: 15 ##### Log in to Harbor registry +Authenticate with Harbor Registry. Depending on how the command is invoked, it behaves differently: + +##### With `-u` / `-p` Flags and `server` + - Opens the login view to obtain new credentials. + - Updates the config file with the new credentials. + - If the specified credential name already exists, it updates the existing entry. + - If the credential name does not exist, it adds a new entry for the credential. + +##### Without `-u` / `-p` Flags and `server` +a. No Existing Credentials in Config: + - Opens the login view to input credentials. + - Stores the entered credentials in the config file. + +b. Existing Credentials in Config: + - Uses the stored credentials from the config file. + - Skips the login view and proceeds to authenticate using the existing credentials. + +For more info on the harbor-cli config management see the [harbor config docs](harbor-config.md) + ### Synopsis Authenticate with Harbor Registry. @@ -36,4 +55,5 @@ harbor login [server] [flags] ### SEE ALSO * [harbor](harbor.md) - Official Harbor CLI +* [harbor config](harbor-config.md) - Harbor Config Management diff --git a/doc/cli-docs/harbor-project-search.md b/doc/cli-docs/harbor-project-search.md new file mode 100644 index 00000000..86b92d0f --- /dev/null +++ b/doc/cli-docs/harbor-project-search.md @@ -0,0 +1,32 @@ +--- +title: harbor project search +weight: 20 +--- +## harbor project search + +### Description + +##### search project based on their names + +```sh +harbor project search [flags] +``` + +### Options + +```sh + -h, --help help for search +``` + +### 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 project](harbor-project.md) - Manage projects and assign resources to them + diff --git a/doc/cli-docs/harbor-repo-search.md b/doc/cli-docs/harbor-repo-search.md new file mode 100644 index 00000000..de5c0494 --- /dev/null +++ b/doc/cli-docs/harbor-repo-search.md @@ -0,0 +1,32 @@ +--- +title: harbor repo search +weight: 30 +--- +## harbor repo search + +### Description + +##### search repository based on their names + +```sh +harbor repo search [flags] +``` + +### Options + +```sh + -h, --help help for search +``` + +### 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 repo](harbor-repo.md) - Manage repositories + diff --git a/doc/cli-docs/harbor-repo-view.md b/doc/cli-docs/harbor-repo-view.md new file mode 100644 index 00000000..9b5855e3 --- /dev/null +++ b/doc/cli-docs/harbor-repo-view.md @@ -0,0 +1,42 @@ +--- +title: harbor repo view +weight: 55 +--- +## harbor repo view + +### Description + +##### Get repository information + +### Synopsis + +Get information of a particular repository in a project + +```sh +harbor repo view [flags] +``` + +### Examples + +```sh + harbor repo view / +``` + +### Options + +```sh + -h, --help help for view +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor repo](harbor-repo.md) - Manage repositories + diff --git a/doc/cli-docs/harbor-schedule-list.md b/doc/cli-docs/harbor-schedule-list.md new file mode 100644 index 00000000..b6e9d3f0 --- /dev/null +++ b/doc/cli-docs/harbor-schedule-list.md @@ -0,0 +1,34 @@ +--- +title: harbor schedule list +weight: 70 +--- +## harbor schedule list + +### Description + +##### show all schedule jobs in Harbor + +```sh +harbor schedule list [flags] +``` + +### Options + +```sh + -h, --help help for list + --page int Page number (default 1) + --page-size int Size of per page (default 10) +``` + +### 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 schedule](harbor-schedule.md) - Schedule jobs in Harbor + diff --git a/doc/cli-docs/harbor-schedule.md b/doc/cli-docs/harbor-schedule.md new file mode 100644 index 00000000..65798692 --- /dev/null +++ b/doc/cli-docs/harbor-schedule.md @@ -0,0 +1,29 @@ +--- +title: harbor schedule +weight: 25 +--- +## harbor schedule + +### Description + +##### Schedule jobs in Harbor + +### Options + +```sh + -h, --help help for schedule +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor](harbor.md) - Official Harbor CLI +* [harbor schedule list](harbor-schedule-list.md) - show all schedule jobs in Harbor + diff --git a/doc/cli-docs/harbor.md b/doc/cli-docs/harbor.md index 5b7f98e8..9afa230b 100644 --- a/doc/cli-docs/harbor.md +++ b/doc/cli-docs/harbor.md @@ -27,7 +27,7 @@ harbor help ### Options ```sh - --config string config file (default is $HOME/.harbor/config.yaml) (default "/home/user/.harbor/config.yaml") + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) -h, --help help for harbor -o, --output-format string Output format. One of: json|yaml -v, --verbose verbose output From f1855332cfc5b96271ca8fe8d2461b6e2bb8d576 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Mon, 2 Dec 2024 14:00:58 +0100 Subject: [PATCH 18/41] Added pwd encryption and key ring usage. The encryption key is stored in the keyring with a specified user and service Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/login.go | 18 ++++++- go.mod | 6 ++- go.sum | 10 ++++ pkg/utils/client.go | 25 +++++++--- pkg/utils/encryption.go | 83 ++++++++++++++++++++++++++++++++ pkg/views/project/create/view.go | 5 +- 6 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 pkg/utils/encryption.go diff --git a/cmd/harbor/root/login.go b/cmd/harbor/root/login.go index 8c02699a..016597e3 100644 --- a/cmd/harbor/root/login.go +++ b/cmd/harbor/root/login.go @@ -130,10 +130,26 @@ func runLogin(opts login.LoginView) error { return fmt.Errorf("login failed, please check your credentials: %s", err) } + if err := utils.GenerateEncryptionKey(); err != nil { + fmt.Println("Encryption key already exists or could not be created:", err) + } + + key, err := utils.GetEncryptionKey() + if err != nil { + fmt.Println("Error getting encryption key:", err) + return fmt.Errorf("failed to get encryption key: %s", err) + } + + encryptedPassword, err := utils.Encrypt(key, []byte(opts.Password)) + if err != nil { + fmt.Println("Error encrypting password:", err) + return fmt.Errorf("failed to encrypt password: %s", err) + } + cred := utils.Credential{ Name: opts.Name, Username: opts.Username, - Password: opts.Password, + Password: encryptedPassword, ServerAddress: opts.Server, } harborData, err := utils.GetCurrentHarborData() diff --git a/go.mod b/go.mod index 14b1fa00..4764bf2a 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( ) require ( + al.essio.dev/pkg/shellescape v1.5.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/catppuccin/go v0.2.0 // indirect @@ -23,10 +24,12 @@ require ( github.com/charmbracelet/x/input v0.1.3 // indirect github.com/charmbracelet/x/term v0.1.1 // indirect github.com/charmbracelet/x/windows v0.1.2 // indirect + github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -50,6 +53,7 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/zalando/go-keyring v0.2.6 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/sync v0.8.0 // indirect @@ -85,7 +89,7 @@ require ( go.opentelemetry.io/otel/metric v1.31.0 // indirect go.opentelemetry.io/otel/sdk v1.28.0 // indirect go.opentelemetry.io/otel/trace v1.31.0 // indirect - golang.org/x/sys v0.25.0 // indirect + golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.18.0 // indirect ) diff --git a/go.sum b/go.sum index a9cd7625..edbf666c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -27,6 +29,8 @@ github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKU github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= +github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -64,6 +68,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/goharbor/go-client v0.210.0 h1:QwgLcWNSC3MFhBe7lq3BxDPtKQiD3k6hf6Lt26NChOI= github.com/goharbor/go-client v0.210.0/go.mod h1:XMWHucuHU9VTRx6U6wYwbRuyCVhE6ffJGRjaeo0nvwo= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -158,6 +164,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= +github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4= go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= @@ -179,6 +187,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= diff --git a/pkg/utils/client.go b/pkg/utils/client.go index 388e380a..03b66995 100644 --- a/pkg/utils/client.go +++ b/pkg/utils/client.go @@ -3,7 +3,6 @@ package utils import ( "context" "fmt" - "os" "sync" "github.com/goharbor/go-client/pkg/harbor" @@ -30,7 +29,7 @@ func GetClient() (*v2client.HarborAPI, error) { return } - clientInstance = GetClientByCredentialName(credentialName) + clientInstance, clientErr = GetClientByCredentialName(credentialName) if clientErr != nil { log.Errorf("failed to initialize client: %v", clientErr) return @@ -58,16 +57,28 @@ func GetClientByConfig(clientConfig *harbor.ClientSetConfig) *v2client.HarborAPI } // Returns Harbor v2 client after resolving the credential name -func GetClientByCredentialName(credentialName string) *v2client.HarborAPI { +func GetClientByCredentialName(credentialName string) (*v2client.HarborAPI, error) { credential, err := GetCredentials(credentialName) if err != nil { - fmt.Print(err) - os.Exit(1) + return nil, fmt.Errorf("failed to get credentials: %w", err) } + + // Get encryption key + key, err := GetEncryptionKey() + if err != nil { + return nil, fmt.Errorf("failed to get encryption key: %w", err) + } + + // Decrypt password + decryptedPassword, err := Decrypt(key, string(credential.Password)) + if err != nil { + return nil, fmt.Errorf("failed to decrypt password: %w", err) + } + clientConfig := &harbor.ClientSetConfig{ URL: credential.ServerAddress, Username: credential.Username, - Password: credential.Password, + Password: decryptedPassword, } - return GetClientByConfig(clientConfig) + return GetClientByConfig(clientConfig), nil } diff --git a/pkg/utils/encryption.go b/pkg/utils/encryption.go new file mode 100644 index 00000000..c5643bbb --- /dev/null +++ b/pkg/utils/encryption.go @@ -0,0 +1,83 @@ +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + + "github.com/zalando/go-keyring" +) + +const KeyringService = "harbor-cli" +const KeyringUser = "harbor-cli-encryption-key" + +func GenerateEncryptionKey() error { + key := make([]byte, 32) // AES-256 key + if _, err := rand.Read(key); err != nil { + return fmt.Errorf("failed to generate encryption key: %w", err) + } + return keyring.Set(KeyringService, KeyringUser, base64.StdEncoding.EncodeToString(key)) +} + +func GetEncryptionKey() ([]byte, error) { + keyBase64, err := keyring.Get(KeyringService, KeyringUser) + if err != nil { + return nil, fmt.Errorf("failed to retrieve encryption key: %w", err) + } + return base64.StdEncoding.DecodeString(keyBase64) +} + +func Encrypt(key, plaintext []byte) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", fmt.Errorf("failedto create cipher: %w", err) + } + + // GCM mode + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("failed to create GCM: %w", err) + } + + nonce := make([]byte, aesGCM.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", fmt.Errorf("failed to generate nonce: %w", err) + } + + ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil) + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +func Decrypt(key []byte, ciphertext string) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", fmt.Errorf("failed to create cipher: %w", err) + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("failed to create GCM: %w", err) + } + + data, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", fmt.Errorf("failed to decode ciphertext: %w", err) + } + + nonceSize := aesGCM.NonceSize() + if len(data) < nonceSize { + return "", fmt.Errorf("ciphertext too short") + } + + nonce := data[:nonceSize] + ciphertextBytes := data[nonceSize:] + plaintext, err := aesGCM.Open(nil, nonce, ciphertextBytes, nil) + if err != nil { + return "", fmt.Errorf("failed to decrypt ciphertext: %w", err) + } + + return string(plaintext), nil +} diff --git a/pkg/views/project/create/view.go b/pkg/views/project/create/view.go index 89ea0c79..c2ed9fd0 100644 --- a/pkg/views/project/create/view.go +++ b/pkg/views/project/create/view.go @@ -23,9 +23,10 @@ type CreateView struct { func getRegistryList() (*registry.ListRegistriesOK, error) { credentialName := viper.GetString("current-credential-name") - client := utils.GetClientByCredentialName(credentialName) + client, err := utils.GetClientByCredentialName(credentialName) ctx := context.Background() - response, err := client.Registry.ListRegistries(ctx, ®istry.ListRegistriesParams{}) + var response *registry.ListRegistriesOK + response, err = client.Registry.ListRegistries(ctx, ®istry.ListRegistriesParams{}) if err != nil { return nil, err From 9502ce1d2c57fbaf7b8e811a7bf7d4839a968c86 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Mon, 2 Dec 2024 14:03:26 +0100 Subject: [PATCH 19/41] Fix: compare existing pwd hash with encrypted pwd Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/harbor/root/login.go b/cmd/harbor/root/login.go index 016597e3..d97089f5 100644 --- a/cmd/harbor/root/login.go +++ b/cmd/harbor/root/login.go @@ -161,7 +161,7 @@ func runLogin(opts login.LoginView) error { existingCred, err := utils.GetCredentials(opts.Name) if err == nil { if existingCred.Username == opts.Username && existingCred.ServerAddress == opts.Server { - if existingCred.Password == opts.Password { + if existingCred.Password == encryptedPassword { log.Warn("Credentials already exist in the config file. They were not added again.") return nil } else { From 3a5676a521c59f0fdf432c2344efb994bb448374 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Mon, 2 Dec 2024 14:09:37 +0100 Subject: [PATCH 20/41] Fix: Ineffective use of err in view.go Signed-off-by: Patrick Eschenbach --- pkg/views/project/create/view.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/views/project/create/view.go b/pkg/views/project/create/view.go index c2ed9fd0..b5c70ea1 100644 --- a/pkg/views/project/create/view.go +++ b/pkg/views/project/create/view.go @@ -24,6 +24,9 @@ type CreateView struct { func getRegistryList() (*registry.ListRegistriesOK, error) { credentialName := viper.GetString("current-credential-name") client, err := utils.GetClientByCredentialName(credentialName) + if err != nil { + return nil, err + } ctx := context.Background() var response *registry.ListRegistriesOK response, err = client.Registry.ListRegistries(ctx, ®istry.ListRegistriesParams{}) From d911fc8aa3deb4606839a0180e9d9782c6d7a943 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Wed, 4 Dec 2024 19:20:56 +0100 Subject: [PATCH 21/41] Added interface for keyring provider; added mockKeyRing provider; Added test for encryption functions; Updated config and login tests with mock key ring provider Signed-off-by: Patrick Eschenbach --- pkg/utils/encryption.go | 33 ++++++++++++++++++++++++++---- pkg/utils/encryption_mock.go | 34 +++++++++++++++++++++++++++++++ test/e2e/config_test.go | 10 +++++++++ test/e2e/encryption_test.go | 39 ++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 pkg/utils/encryption_mock.go create mode 100644 test/e2e/encryption_test.go diff --git a/pkg/utils/encryption.go b/pkg/utils/encryption.go index c5643bbb..a57476b6 100644 --- a/pkg/utils/encryption.go +++ b/pkg/utils/encryption.go @@ -11,6 +11,32 @@ import ( "github.com/zalando/go-keyring" ) +type KeyringProvider interface { + Set(service, user, password string) error + Get(service, user string) (string, error) + Delete(service, user string) error +} + +type SystemKeyring struct{} + +func (s *SystemKeyring) Set(service, user, password string) error { + return keyring.Set(service, user, password) +} + +func (s *SystemKeyring) Get(service, user string) (string, error) { + return keyring.Get(service, user) +} + +func (s *SystemKeyring) Delete(service, user string) error { + return keyring.Delete(service, user) +} + +var keyringProvider KeyringProvider = &SystemKeyring{} + +func SetKeyringProvider(provider KeyringProvider) { + keyringProvider = provider +} + const KeyringService = "harbor-cli" const KeyringUser = "harbor-cli-encryption-key" @@ -19,11 +45,11 @@ func GenerateEncryptionKey() error { if _, err := rand.Read(key); err != nil { return fmt.Errorf("failed to generate encryption key: %w", err) } - return keyring.Set(KeyringService, KeyringUser, base64.StdEncoding.EncodeToString(key)) + return keyringProvider.Set(KeyringService, KeyringUser, base64.StdEncoding.EncodeToString(key)) } func GetEncryptionKey() ([]byte, error) { - keyBase64, err := keyring.Get(KeyringService, KeyringUser) + keyBase64, err := keyringProvider.Get(KeyringService, KeyringUser) if err != nil { return nil, fmt.Errorf("failed to retrieve encryption key: %w", err) } @@ -33,10 +59,9 @@ func GetEncryptionKey() ([]byte, error) { func Encrypt(key, plaintext []byte) (string, error) { block, err := aes.NewCipher(key) if err != nil { - return "", fmt.Errorf("failedto create cipher: %w", err) + return "", fmt.Errorf("failed to create cipher: %w", err) } - // GCM mode aesGCM, err := cipher.NewGCM(block) if err != nil { return "", fmt.Errorf("failed to create GCM: %w", err) diff --git a/pkg/utils/encryption_mock.go b/pkg/utils/encryption_mock.go new file mode 100644 index 00000000..ffd62d96 --- /dev/null +++ b/pkg/utils/encryption_mock.go @@ -0,0 +1,34 @@ +package utils + +import "fmt" + +type MockKeyring struct { + store map[string]map[string]string +} + +func NewMockKeyring() *MockKeyring { + return &MockKeyring{store: make(map[string]map[string]string)} +} + +func (m *MockKeyring) Set(service, user, password string) error { + if m.store[service] == nil { + m.store[service] = make(map[string]string) + } + m.store[service][user] = password + return nil +} + +func (m *MockKeyring) Get(service, user string) (string, error) { + if val, ok := m.store[service][user]; ok { + return val, nil + } + return "", fmt.Errorf("key not found") +} + +func (m *MockKeyring) Delete(service, user string) error { + if _, ok := m.store[service][user]; ok { + delete(m.store[service], user) + return nil + } + return fmt.Errorf("key not found") +} diff --git a/test/e2e/config_test.go b/test/e2e/config_test.go index 239491c5..e9855ce4 100644 --- a/test/e2e/config_test.go +++ b/test/e2e/config_test.go @@ -50,8 +50,15 @@ func ConfigCleanup(t *testing.T, data *utils.HarborData) { data = nil } +func SetMockKeyring(t *testing.T) { + mockKeyring := utils.NewMockKeyring() + utils.SetKeyringProvider(mockKeyring) + defer utils.SetKeyringProvider(&utils.SystemKeyring{}) // Restore original provider after test +} + func Initialize(t *testing.T, tempDir string) *utils.HarborData { utils.ConfigInitialization.Reset() // Reset sync.Once for the test + SetMockKeyring(t) safeSetEnv("XDG_DATA_HOME", filepath.Join(tempDir, ".data")) utils.InitConfig(filepath.Join(tempDir, ".config", "config.yaml"), true) cds := root.RootCmd() @@ -66,6 +73,7 @@ func Initialize(t *testing.T, tempDir string) *utils.HarborData { func Test_Config_EnvVar(t *testing.T) { utils.ConfigInitialization.Reset() // Reset sync.Once for the test + SetMockKeyring(t) tempDir := t.TempDir() safeSetEnv("HARBOR_CLI_CONFIG", filepath.Join(tempDir, "config.yaml")) safeSetEnv("XDG_DATA_HOME", filepath.Join(tempDir, ".data")) @@ -89,6 +97,7 @@ func Test_Config_EnvVar(t *testing.T) { func Test_Config_Vanilla(t *testing.T) { utils.ConfigInitialization.Reset() // Reset sync.Once for the test + SetMockKeyring(t) utils.InitConfig("", false) cds := root.RootCmd() err := cds.Execute() @@ -108,6 +117,7 @@ func Test_Config_Vanilla(t *testing.T) { func Test_Config_Xdg(t *testing.T) { utils.ConfigInitialization.Reset() // Reset sync.Once for the test + SetMockKeyring(t) tempDir := t.TempDir() safeSetEnv("HARBOR_CLI_CONFIG", filepath.Join(tempDir, "config.yaml")) safeSetEnv("XDG_CONFIG_HOME", filepath.Join(tempDir, ".config")) diff --git a/test/e2e/encryption_test.go b/test/e2e/encryption_test.go new file mode 100644 index 00000000..105dd747 --- /dev/null +++ b/test/e2e/encryption_test.go @@ -0,0 +1,39 @@ +package e2e + +import ( + "testing" + + "github.com/goharbor/harbor-cli/pkg/utils" +) + +func Test_EncryptionWithMockKeyring(t *testing.T) { + // Use mock keyring for tests + mockKeyring := utils.NewMockKeyring() + utils.SetKeyringProvider(mockKeyring) + + // Run tests + err := utils.GenerateEncryptionKey() + if err != nil { + t.Fatalf("failed to generate encryption key: %v", err) + } + + key, err := utils.GetEncryptionKey() + if err != nil { + t.Fatalf("failed to get encryption key: %v", err) + } + + plaintext := "my-secret" + encrypted, err := utils.Encrypt(key, []byte(plaintext)) + if err != nil { + t.Fatalf("failed to encrypt: %v", err) + } + + decrypted, err := utils.Decrypt(key, encrypted) + if err != nil { + t.Fatalf("failed to decrypt: %v", err) + } + + if decrypted != plaintext { + t.Fatalf("expected %s but got %s", plaintext, decrypted) + } +} From f408403fc695175b29d8486e5a48872e04b7a3b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:52:51 +0100 Subject: [PATCH 22/41] build(deps): bump github.com/charmbracelet/bubbles from 0.18.0 to 0.20.0 (#189) Bumps [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) from 0.18.0 to 0.20.0. - [Release notes](https://github.com/charmbracelet/bubbles/releases) - [Changelog](https://github.com/charmbracelet/bubbles/blob/master/.goreleaser.yml) - [Commits](https://github.com/charmbracelet/bubbles/compare/v0.18.0...v0.20.0) --- updated-dependencies: - dependency-name: github.com/charmbracelet/bubbles dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: Patrick Eschenbach --- go.mod | 15 +++++++++------ go.sum | 37 +++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 4764bf2a..69340c72 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,16 @@ module github.com/goharbor/harbor-cli go 1.22.5 require ( - github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.26.6 + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.1.0 github.com/charmbracelet/huh v0.5.2 - github.com/charmbracelet/lipgloss v0.12.1 + github.com/charmbracelet/lipgloss v0.13.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 golang.org/x/term v0.6.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -19,12 +20,13 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/catppuccin/go v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.1.4 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240725160154-f9f6568126ec // indirect github.com/charmbracelet/x/input v0.1.3 // indirect github.com/charmbracelet/x/term v0.1.1 // indirect github.com/charmbracelet/x/windows v0.1.2 // indirect github.com/danieljoos/wincred v1.2.2 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect @@ -39,7 +41,6 @@ require ( github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -52,13 +53,15 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect +<<<<<<< HEAD github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/zalando/go-keyring v0.2.6 // indirect +======= +>>>>>>> cb5437e (build(deps): bump github.com/charmbracelet/bubbles from 0.18.0 to 0.20.0 (#189)) go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/sync v0.8.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( diff --git a/go.sum b/go.sum index edbf666c..a544c81d 100644 --- a/go.sum +++ b/go.sum @@ -1,33 +1,35 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= -github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s= -github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= +github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= github.com/charmbracelet/huh v0.5.2 h1:ofeNkJ4iaFnzv46Njhx896DzLUe/j0L2QAf8znwzX4c= github.com/charmbracelet/huh v0.5.2/go.mod h1:Sf7dY0oAn6N/e3sXJFtFX9hdQLrUdO3z7AYollG9bAM= -github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= -github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= -github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= -github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/strings v0.0.0-20240725160154-f9f6568126ec h1:G79PnmxvVeo5FbAip3VPqAaBkOeXExwvhIBUfrXD5MA= github.com/charmbracelet/x/exp/strings v0.0.0-20240725160154-f9f6568126ec/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE= github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU= -github.com/charmbracelet/x/input v0.1.3 h1:oy4TMhyGQsYs/WWJwu1ELUMFnjiUAXwtDf048fHbCkg= -github.com/charmbracelet/x/input v0.1.3/go.mod h1:1gaCOyw1KI9e2j00j/BBZ4ErzRZqa05w0Ghn83yIhKU= -github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= -github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= -github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= -github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= @@ -98,7 +100,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= @@ -109,8 +110,6 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -122,7 +121,6 @@ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -162,10 +160,13 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +<<<<<<< HEAD github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= +======= +>>>>>>> cb5437e (build(deps): bump github.com/charmbracelet/bubbles from 0.18.0 to 0.20.0 (#189)) go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4= go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= From 6bae96dd7f9a6da990bc6efa381aec05326660dd Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Wed, 4 Dec 2024 22:40:15 +0100 Subject: [PATCH 23/41] Explicitely set mock keyring in sub tests Signed-off-by: Patrick Eschenbach --- test/e2e/login_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/e2e/login_test.go b/test/e2e/login_test.go index e3869057..a9d09c2d 100644 --- a/test/e2e/login_test.go +++ b/test/e2e/login_test.go @@ -29,6 +29,8 @@ func Test_Login_Success(t *testing.T) { assert.NoError(t, cmd.Flags().Set("username", "harbor-cli")) assert.NoError(t, cmd.Flags().Set("password", "Harbor12345")) + SetMockKeyring(t) + defer SetMockKeyring(t) err := cmd.Execute() assert.NoError(t, err, "Expected no error for server: %s", serverAddress) }) From e93a24cd69db692a78a2d192c5f0fa35b867908b Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Wed, 4 Dec 2024 22:46:16 +0100 Subject: [PATCH 24/41] Explicitely set mock keyring in sub tests Signed-off-by: Patrick Eschenbach --- test/e2e/config_test.go | 5 ++++- test/e2e/login_test.go | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/test/e2e/config_test.go b/test/e2e/config_test.go index e9855ce4..b0f44c67 100644 --- a/test/e2e/config_test.go +++ b/test/e2e/config_test.go @@ -53,7 +53,10 @@ func ConfigCleanup(t *testing.T, data *utils.HarborData) { func SetMockKeyring(t *testing.T) { mockKeyring := utils.NewMockKeyring() utils.SetKeyringProvider(mockKeyring) - defer utils.SetKeyringProvider(&utils.SystemKeyring{}) // Restore original provider after test + + t.Cleanup(func() { + utils.SetKeyringProvider(&utils.SystemKeyring{}) + }) } func Initialize(t *testing.T, tempDir string) *utils.HarborData { diff --git a/test/e2e/login_test.go b/test/e2e/login_test.go index a9d09c2d..335d9934 100644 --- a/test/e2e/login_test.go +++ b/test/e2e/login_test.go @@ -11,13 +11,15 @@ func Test_Login_Success(t *testing.T) { tempDir := t.TempDir() data := Initialize(t, tempDir) defer ConfigCleanup(t, data) + + SetMockKeyring(t) + cmd := root.LoginCommand() validServerAddresses := []string{ "http://demo.goharbor.io:80", "https://demo.goharbor.io:443", "http://demo.goharbor.io", "https://demo.goharbor.io", - // "demo.goharbor.io", } for _, serverAddress := range validServerAddresses { @@ -29,8 +31,6 @@ func Test_Login_Success(t *testing.T) { assert.NoError(t, cmd.Flags().Set("username", "harbor-cli")) assert.NoError(t, cmd.Flags().Set("password", "Harbor12345")) - SetMockKeyring(t) - defer SetMockKeyring(t) err := cmd.Execute() assert.NoError(t, err, "Expected no error for server: %s", serverAddress) }) From 0eb90cc3b360edde129fdb86a045acc711fccf83 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Sat, 21 Dec 2024 20:16:21 +0100 Subject: [PATCH 25/41] Added config sub command; added functions for set, get, list and clear config items; added a function to update config on disk Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/cmd.go | 2 + cmd/harbor/root/config/cmd.go | 20 +++ cmd/harbor/root/config/delete.go | 148 +++++++++++++++++++++ cmd/harbor/root/config/get.go | 162 +++++++++++++++++++++++ cmd/harbor/root/config/list.go | 48 +++++++ cmd/harbor/root/config/update.go | 219 +++++++++++++++++++++++++++++++ pkg/utils/config.go | 43 ++++++ 7 files changed, 642 insertions(+) create mode 100644 cmd/harbor/root/config/cmd.go create mode 100644 cmd/harbor/root/config/delete.go create mode 100644 cmd/harbor/root/config/get.go create mode 100644 cmd/harbor/root/config/list.go create mode 100644 cmd/harbor/root/config/update.go diff --git a/cmd/harbor/root/cmd.go b/cmd/harbor/root/cmd.go index 8ff6bb73..72e5dcc0 100644 --- a/cmd/harbor/root/cmd.go +++ b/cmd/harbor/root/cmd.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/goharbor/harbor-cli/cmd/harbor/root/artifact" + "github.com/goharbor/harbor-cli/cmd/harbor/root/config" "github.com/goharbor/harbor-cli/cmd/harbor/root/project" "github.com/goharbor/harbor-cli/cmd/harbor/root/registry" repositry "github.com/goharbor/harbor-cli/cmd/harbor/root/repository" @@ -60,6 +61,7 @@ harbor help root.AddCommand( versionCommand(), LoginCommand(), + config.Config(), project.Project(), registry.Registry(), repositry.Repository(), diff --git a/cmd/harbor/root/config/cmd.go b/cmd/harbor/root/config/cmd.go new file mode 100644 index 00000000..bd91163c --- /dev/null +++ b/cmd/harbor/root/config/cmd.go @@ -0,0 +1,20 @@ +package config + +import "github.com/spf13/cobra" + +func Config() *cobra.Command { + cmd := &cobra.Command{ + Use: "config", + Short: "Manage the config of the Harbor Cli", + Long: `Manage repositories in Harbor config`, + } + cmd.AddCommand( + ListConfigCommand(), + GetConfigItemCommand(), + SetConfigItemCommand(), + DeleteConfigItemCommand(), + ) + + return cmd + +} diff --git a/cmd/harbor/root/config/delete.go b/cmd/harbor/root/config/delete.go new file mode 100644 index 00000000..fddb7190 --- /dev/null +++ b/cmd/harbor/root/config/delete.go @@ -0,0 +1,148 @@ +package config + +import ( + "fmt" + "reflect" + "strings" + + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// DeleteConfigItemCommand creates the 'harbor config delete' subcommand, +// allowing you to do: harbor config delete +func DeleteConfigItemCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete (clear) a specific config item", + Example: " harbor config delete credentials.password", + Long: `Clear the value of a specific CLI config item by setting it to its zero value. +Case-insensitive field lookup, but uses the canonical (Go) field name internally.`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + // 1. Load the current config + config, err := utils.GetCurrentHarborConfig() + if err != nil { + logrus.Errorf("Failed to load Harbor config: %v", err) + return + } + + // 2. Parse the user-supplied item path (e.g., "credentials.password") + itemPath := strings.Split(args[0], ".") + + // 3. Reflection-based delete (zero out) + actualSegments := []string{} + if err := deleteValueInConfig(config, itemPath, &actualSegments); err != nil { + logrus.Error(err) + return + } + + // 4. Persist the updated config to disk + if err := utils.UpdateConfigFile(config); err != nil { + logrus.Errorf("Failed to save updated config: %v", err) + return + } + + // 5. Confirm to the user + canonicalPath := strings.Join(actualSegments, ".") + logrus.Infof("Successfully cleared %s", canonicalPath) + }, + } + + return cmd +} + +// deleteValueInConfig checks whether the user is deleting something +// under "credentials" (i.e., the current credential) or a top-level field. +func deleteValueInConfig(config *utils.HarborConfig, path []string, actualSegments *[]string) error { + if len(path) == 0 { + return fmt.Errorf("no config item specified") + } + + // If the first segment is "credentials", then we pivot to the current credential. + if strings.EqualFold(path[0], "credentials") { + *actualSegments = append(*actualSegments, "Credentials") + + // find the current credential + currentCredName := config.CurrentCredentialName + var currentCred *utils.Credential + for i := range config.Credentials { + if strings.EqualFold(config.Credentials[i].Name, currentCredName) { + currentCred = &config.Credentials[i] + break + } + } + if currentCred == nil { + return fmt.Errorf("no matching credential found for '%s'", currentCredName) + } + + // Remove "credentials" from the path, and delete (zero) the value in that credential + return deleteNestedValue(currentCred, path[1:], actualSegments) + } + + // Otherwise, we delete a field in the main HarborConfig struct + return deleteNestedValue(config, path, actualSegments) +} + +// deleteNestedValue navigates a pointer to a struct, following the path segments +// in a case-insensitive manner, until the last segment, where it sets the field +// to its zero value. +func deleteNestedValue(obj interface{}, path []string, actualSegments *[]string) error { + // We require obj to be a pointer to a struct so we can modify it. + val := reflect.ValueOf(obj) + if val.Kind() != reflect.Ptr { + return fmt.Errorf("object must be a pointer to a struct, got %s", val.Kind()) + } + val = val.Elem() // dereference pointer + + for i, segment := range path { + if val.Kind() != reflect.Struct { + return fmt.Errorf("cannot traverse non-struct for segment '%s'", segment) + } + t := val.Type() + + // Case-insensitive field lookup + fieldIndex := -1 + for j := 0; j < val.NumField(); j++ { + if strings.EqualFold(t.Field(j).Name, segment) { + fieldIndex = j + break + } + } + if fieldIndex < 0 { + return fmt.Errorf("config item '%s' does not exist", segment) + } + + field := t.Field(fieldIndex) + fieldValue := val.Field(fieldIndex) + + // Record the actual field name + *actualSegments = append(*actualSegments, field.Name) + + // If this is NOT the last path segment, move deeper + if i < len(path)-1 { + // If the field is a pointer and nil, we can't go deeper + if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { + return fmt.Errorf("field '%s' is nil and cannot be traversed", field.Name) + } + // Descend + val = fieldValue + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + continue + } + + // If this is the last segment, set the field to zero value + if !fieldValue.CanSet() { + return fmt.Errorf("cannot delete (set zero value) for field '%s'", field.Name) + } + + // The "zero" value for that field can be obtained with reflect.Zero(). + zeroVal := reflect.Zero(fieldValue.Type()) + fieldValue.Set(zeroVal) + } + + return nil +} diff --git a/cmd/harbor/root/config/get.go b/cmd/harbor/root/config/get.go new file mode 100644 index 00000000..743fece9 --- /dev/null +++ b/cmd/harbor/root/config/get.go @@ -0,0 +1,162 @@ +package config + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +// GetConfigItemCommand creates the 'harbor config get' subcommand. +func GetConfigItemCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "get ", + Short: "Get a specific config item", + Example: ` harbor config get credentials.username`, + Long: `Get the value of a specific CLI config item`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + // 1. Load config + config, err := utils.GetCurrentHarborConfig() + if err != nil { + logrus.Errorf("Failed to get config: %v", err) + return + } + + // 2. Parse the user-supplied item path (e.g. "credentials.username") + itemPath := strings.Split(args[0], ".") + + // 3. Get the value from the config (filtering credentials if needed) + // also capture the actual field name segments for pretty output + actualSegments := []string{} + result, err := getValueFromConfig(config, itemPath, &actualSegments) + if err != nil { + logrus.Error(err) + return + } + + // 4. Prepare the final output as a map so we can render easily in JSON/YAML + // We join the actual field names with "." for the "canonical" path. + canonicalPath := strings.Join(actualSegments, ".") + output := map[string]interface{}{ + canonicalPath: result, + } + + // 5. Determine output format + formatFlag := viper.GetString("output-format") + switch formatFlag { + case "json": + data, err := json.MarshalIndent(output, "", " ") + if err != nil { + logrus.Errorf("Failed to marshal output to JSON: %v", err) + return + } + fmt.Println(string(data)) + + case "yaml", "": + data, err := yaml.Marshal(output) + if err != nil { + logrus.Errorf("Failed to marshal output to YAML: %v", err) + return + } + fmt.Println(string(data)) + + default: + logrus.Errorf("Unsupported output format: %s", formatFlag) + } + }, + } + + return cmd +} + +// getValueFromConfig decides if the user requested something under "credentials" +// and if so, filters down to the current credential; otherwise, it just +// searches in the top-level config object. +// +// We also accept a pointer to 'actualSegments', so that if the user typed +// "credentials.Username", we can store the correct name for each field. E.g. "Credentials" -> "Username". +func getValueFromConfig(config *utils.HarborConfig, path []string, actualSegments *[]string) (interface{}, error) { + if len(path) == 0 { + return nil, fmt.Errorf("no config item specified") + } + + // If the first segment is "credentials", we pivot to the "current credential" + // and append the actual field name "Credentials" to 'actualSegments'. + if strings.EqualFold(path[0], "credentials") { + *actualSegments = append(*actualSegments, "Credentials") + + // Find the current credential + currentCredName := config.CurrentCredentialName + var currentCred *utils.Credential + for _, cred := range config.Credentials { + if strings.EqualFold(cred.Name, currentCredName) { + currentCred = &cred + break + } + } + if currentCred == nil { + return nil, fmt.Errorf("no matching credential found for '%s'", currentCredName) + } + + // Remove "credentials" from the path, keep the rest + return getNestedValue(*currentCred, path[1:], actualSegments) + } + + // Otherwise, search in the overall config struct + return getNestedValue(*config, path, actualSegments) +} + +// getNestedValue uses reflection to walk through struct fields +// (case-insensitive) according to the provided path. +// +// 'actualSegments' is updated with the actual field names as we go. +func getNestedValue(obj interface{}, path []string, actualSegments *[]string) (interface{}, error) { + current := reflect.ValueOf(obj) + + for _, key := range path { + if current.Kind() == reflect.Ptr { + current = current.Elem() + } + if current.Kind() != reflect.Struct { + return nil, fmt.Errorf("cannot traverse non-struct for key '%s'", key) + } + + // Find the actual field by name, ignoring case + var foundField reflect.StructField + var fieldValue reflect.Value + fieldFound := false + + t := current.Type() + for i := 0; i < current.NumField(); i++ { + field := t.Field(i) + if strings.EqualFold(field.Name, key) { + foundField = field + fieldValue = current.Field(i) + fieldFound = true + break + } + } + if !fieldFound { + return nil, fmt.Errorf("config item '%s' does not exist", key) + } + + // Record the *actual* field name in our slice + *actualSegments = append(*actualSegments, foundField.Name) + + // Descend for the next iteration + current = fieldValue + } + + // Finally, if we ended on a pointer, dereference it + if current.Kind() == reflect.Ptr { + current = current.Elem() + } + return current.Interface(), nil +} diff --git a/cmd/harbor/root/config/list.go b/cmd/harbor/root/config/list.go new file mode 100644 index 00000000..84334835 --- /dev/null +++ b/cmd/harbor/root/config/list.go @@ -0,0 +1,48 @@ +package config + +import ( + "fmt" + + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +func ListConfigCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List config items", + Example: ` harbor config list`, + Long: `Get information of all CLI config items`, + Args: cobra.MaximumNArgs(0), + Run: func(cmd *cobra.Command, args []string) { + config, err := utils.GetCurrentHarborConfig() + if err != nil { + logrus.Errorf("Failed to get config: %v", err) + return + } + + // Get the output format + formatFlag := viper.GetString("output-format") + if formatFlag != "" { + // Use utils.PrintFormat if available + err = utils.PrintFormat(config, formatFlag) + if err != nil { + logrus.Errorf("Failed to print config: %v", err) + } + } else { + // Default to YAML format + data, err := yaml.Marshal(config) + if err != nil { + logrus.Errorf("Failed to marshal config to YAML: %v", err) + return + } + fmt.Println(string(data)) + } + }, + } + + return cmd +} diff --git a/cmd/harbor/root/config/update.go b/cmd/harbor/root/config/update.go new file mode 100644 index 00000000..adeec1ba --- /dev/null +++ b/cmd/harbor/root/config/update.go @@ -0,0 +1,219 @@ +package config + +import ( + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// SetConfigItemCommand creates the 'harbor config set' subcommand, +// allowing you to do: harbor config set . +func SetConfigItemCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "set ", + Short: "Set a specific config item", + Example: " harbor config set credentials.password myNewSecret", + Long: `Set the value of a specific CLI config item. +Case-insensitive field lookup, but uses the canonical (Go) field name internally.`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + // 1. Load the current config + config, err := utils.GetCurrentHarborConfig() + if err != nil { + logrus.Errorf("Failed to load Harbor config: %v", err) + return + } + + // 2. Parse the user-supplied item path (e.g., "credentials.password") + itemPath := strings.Split(args[0], ".") + newValue := args[1] + + // 3. Reflection-based set + actualSegments := []string{} + if err := setValueInConfig(config, itemPath, newValue, &actualSegments); err != nil { + logrus.Error(err) + return + } + + // 4. Persist the updated config to disk + if err := utils.UpdateConfigFile(config); err != nil { + logrus.Errorf("Failed to save updated config: %v", err) + return + } + + // 5. Confirm to the user + canonicalPath := strings.Join(actualSegments, ".") + logrus.Infof("Successfully updated %s to '%s'", canonicalPath, newValue) + }, + } + + return cmd +} + +// setValueInConfig checks whether the user is updating something +// under "credentials" (i.e., the current credential) or a top-level field. +func setValueInConfig(config *utils.HarborConfig, path []string, newValue string, actualSegments *[]string) error { + if len(path) == 0 { + return fmt.Errorf("no config item specified") + } + + // If the first segment is "credentials", then we pivot to the current credential. + if strings.EqualFold(path[0], "credentials") { + *actualSegments = append(*actualSegments, "Credentials") + + // find the current credential + currentCredName := config.CurrentCredentialName + var currentCred *utils.Credential + for i := range config.Credentials { + if strings.EqualFold(config.Credentials[i].Name, currentCredName) { + currentCred = &config.Credentials[i] + break + } + } + if currentCred == nil { + return fmt.Errorf("no matching credential found for '%s'", currentCredName) + } + + // Remove "credentials" from the path, and set the value in that credential + return setNestedValue(currentCred, path[1:], newValue, actualSegments) + } + + // Otherwise, we set a field in the main HarborConfig struct + return setNestedValue(config, path, newValue, actualSegments) +} + +// setNestedValue navigates a pointer to a struct, following the path segments +// in a case-insensitive manner, until the last segment, where it sets the value. +// +// If the last segment is Credentials.Password, it encrypts the user-supplied +// password before storing it. +func setNestedValue(obj interface{}, path []string, newValue string, actualSegments *[]string) error { + // We require obj to be a pointer to a struct so we can modify it. + val := reflect.ValueOf(obj) + if val.Kind() != reflect.Ptr { + return fmt.Errorf("object must be a pointer to a struct, got %s", val.Kind()) + } + val = val.Elem() // dereference pointer + + for i, segment := range path { + if val.Kind() != reflect.Struct { + return fmt.Errorf("cannot traverse non-struct for segment '%s'", segment) + } + t := val.Type() + + // Case-insensitive field lookup + fieldIndex := -1 + for j := 0; j < val.NumField(); j++ { + if strings.EqualFold(t.Field(j).Name, segment) { + fieldIndex = j + break + } + } + if fieldIndex < 0 { + return fmt.Errorf("config item '%s' does not exist", segment) + } + + field := t.Field(fieldIndex) + fieldValue := val.Field(fieldIndex) + + // Record the actual field name + *actualSegments = append(*actualSegments, field.Name) + + // If this is NOT the last path segment, move deeper + if i < len(path)-1 { + // If the field is a pointer and nil, allocate a new instance + if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { + newElem := reflect.New(fieldValue.Type().Elem()) + fieldValue.Set(newElem) + } + // Descend + val = fieldValue + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + continue + } + + // If this is the last segment, set the value + if !fieldValue.CanSet() { + return fmt.Errorf("cannot set field '%s'", field.Name) + } + + switch fieldValue.Kind() { + case reflect.String: + // Special case: If we are setting Credentials.Password, encrypt it + // We'll check the last two actual segments, e.g. ["Credentials", "Password"]. + if isCredentialsPassword(*actualSegments) { + encrypted, err := encryptPassword(newValue) + if err != nil { + return err + } + fieldValue.SetString(encrypted) + } else { + fieldValue.SetString(newValue) + } + + case reflect.Bool: + boolVal, err := strconv.ParseBool(newValue) + if err != nil { + return fmt.Errorf("field '%s' expects a bool, but got '%s'", field.Name, newValue) + } + fieldValue.SetBool(boolVal) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + intVal, err := strconv.ParseInt(newValue, 10, 64) + if err != nil { + return fmt.Errorf("field '%s' expects an integer, but got '%s'", field.Name, newValue) + } + fieldValue.SetInt(intVal) + + // If you need to handle other types (e.g. float, slice), add them here. + default: + return fmt.Errorf( + "unsupported field type '%s' for field '%s'", + fieldValue.Kind().String(), field.Name, + ) + } + } + + return nil +} + +// isCredentialsPassword checks if the actualSegments match ["Credentials", "Password"] +// (case-insensitive). +func isCredentialsPassword(actualSegments []string) bool { + if len(actualSegments) < 2 { + return false + } + // e.g. last two items might be Credentials, Password + last := actualSegments[len(actualSegments)-1] + secondLast := actualSegments[len(actualSegments)-2] + return strings.EqualFold(secondLast, "Credentials") && + strings.EqualFold(last, "Password") +} + +// encryptPassword uses your existing utility functions to generate/retrieve a key +// and return an encrypted version of the supplied password. +func encryptPassword(plaintext string) (string, error) { + // Make sure a key exists + if err := utils.GenerateEncryptionKey(); err != nil { + // It's okay if the key already exists; that might not be a fatal error for you + logrus.Debugf("Encryption key might already exist: %v", err) + } + + key, err := utils.GetEncryptionKey() + if err != nil { + return "", fmt.Errorf("failed to get encryption key: %w", err) + } + + encrypted, err := utils.Encrypt(key, []byte(plaintext)) + if err != nil { + return "", fmt.Errorf("failed to encrypt password: %w", err) + } + return encrypted, nil +} diff --git a/pkg/utils/config.go b/pkg/utils/config.go index ec83542c..fbeff551 100644 --- a/pkg/utils/config.go +++ b/pkg/utils/config.go @@ -348,6 +348,49 @@ func CreateConfigFile(configPath string) error { return nil } +// UpdateConfigFile updates the YAML config file on disk with the +// values in the given HarborConfig, and also updates the in-memory CurrentHarborConfig. +func UpdateConfigFile(config *HarborConfig) error { + configMutex.Lock() + defer configMutex.Unlock() + + // Ensure we know where to write the config + if CurrentHarborData == nil { + return errors.New("harbor data is nil – check that your config initialization completed") + } + configPath := CurrentHarborData.ConfigPath + + // Ensure the file actually exists + if _, err := os.Stat(configPath); os.IsNotExist(err) { + return fmt.Errorf("config file does not exist at %s", configPath) + } else if err != nil { + return fmt.Errorf("error checking config file: %v", err) + } + + // Read the existing config file via viper + v := viper.New() + v.SetConfigFile(configPath) + v.SetConfigType("yaml") + if err := v.ReadInConfig(); err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + // Overwrite the specific fields we care about + v.Set("current-credential-name", config.CurrentCredentialName) + v.Set("credentials", config.Credentials) + + // Write back to disk + if err := v.WriteConfig(); err != nil { + return fmt.Errorf("failed to write updated config file: %w", err) + } + + // Also update our global in-memory config + CurrentHarborConfig = config + + log.Infof("Updated config file at %s", configPath) + return nil +} + func GetCredentials(credentialName string) (Credential, error) { currentConfig, err := GetCurrentHarborConfig() if err != nil { From d2916be5da9336db34fa2b9c8c6ce57b978f06b8 Mon Sep 17 00:00:00 2001 From: Alan Tang Date: Tue, 10 Dec 2024 02:05:23 +0800 Subject: [PATCH 26/41] bug: fix push-latest-images and publish-release (#279) Signed-off-by: Alan Tang Signed-off-by: Patrick Eschenbach --- .github/workflows/default.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/default.yaml b/.github/workflows/default.yaml index c1953237..7138e444 100644 --- a/.github/workflows/default.yaml +++ b/.github/workflows/default.yaml @@ -59,7 +59,7 @@ jobs: args: build-dev --platform linux/amd64 export --path=./harbor-dev push-latest-images: - if: github.event.pull_request == null && !startsWith(github.ref, 'refs/tags/v') + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) needs: - lint - test-code @@ -84,7 +84,7 @@ jobs: publish-release: - if: startsWith(github.ref, 'refs/tags/v') + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) needs: - lint - test-code From 7bee62c7536b0dea618c5655d7a85d199adc0a77 Mon Sep 17 00:00:00 2001 From: ALTHAF <114910365+Althaf66@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:12:47 +0530 Subject: [PATCH 27/41] Add: label commands (#95) * created new command label and its subcommands Signed-off-by: Althaf66 * modified label cmd Signed-off-by: Althaf66 * created update label cmd Signed-off-by: ALTHAF * modified label update cmd Signed-off-by: ALTHAF * modified label update cmd Signed-off-by: ALTHAF * added 32 color choice for label Signed-off-by: ALTHAF --------- Signed-off-by: Althaf66 Signed-off-by: ALTHAF <114910365+Althaf66@users.noreply.github.com> Signed-off-by: ALTHAF Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/cmd.go | 2 + cmd/harbor/root/labels/add.go | 1 - cmd/harbor/root/labels/cmd.go | 6 ++ cmd/harbor/root/labels/create.go | 56 +++++++++++++ cmd/harbor/root/labels/delete.go | 39 +++++++++ cmd/harbor/root/labels/list.go | 41 ++++++++++ cmd/harbor/root/labels/update.go | 76 ++++++++++++++++++ cmd/harbor/root/registry/delete.go | 2 +- pkg/api/label_handler.go | 124 +++++++++++++++++++++++++++++ pkg/api/types.go | 14 ++-- pkg/prompt/prompt.go | 11 +++ pkg/views/label/create/view.go | 82 +++++++++++++++++++ pkg/views/label/list/view.go | 40 ++++++++++ pkg/views/label/select/view.go | 36 +++++++++ pkg/views/label/update/view.go | 76 ++++++++++++++++++ 15 files changed, 598 insertions(+), 8 deletions(-) delete mode 100644 cmd/harbor/root/labels/add.go create mode 100644 cmd/harbor/root/labels/create.go create mode 100644 cmd/harbor/root/labels/list.go create mode 100644 cmd/harbor/root/labels/update.go create mode 100644 pkg/api/label_handler.go create mode 100644 pkg/views/label/create/view.go create mode 100644 pkg/views/label/list/view.go create mode 100644 pkg/views/label/select/view.go create mode 100644 pkg/views/label/update/view.go diff --git a/cmd/harbor/root/cmd.go b/cmd/harbor/root/cmd.go index 72e5dcc0..e439602c 100644 --- a/cmd/harbor/root/cmd.go +++ b/cmd/harbor/root/cmd.go @@ -5,6 +5,7 @@ import ( "github.com/goharbor/harbor-cli/cmd/harbor/root/artifact" "github.com/goharbor/harbor-cli/cmd/harbor/root/config" + "github.com/goharbor/harbor-cli/cmd/harbor/root/labels" "github.com/goharbor/harbor-cli/cmd/harbor/root/project" "github.com/goharbor/harbor-cli/cmd/harbor/root/registry" repositry "github.com/goharbor/harbor-cli/cmd/harbor/root/repository" @@ -69,6 +70,7 @@ harbor help artifact.Artifact(), HealthCommand(), schedule.Schedule(), + labels.Labels(), ) return root diff --git a/cmd/harbor/root/labels/add.go b/cmd/harbor/root/labels/add.go deleted file mode 100644 index d8e69cdb..00000000 --- a/cmd/harbor/root/labels/add.go +++ /dev/null @@ -1 +0,0 @@ -package labels diff --git a/cmd/harbor/root/labels/cmd.go b/cmd/harbor/root/labels/cmd.go index 68c4ef73..8874532d 100644 --- a/cmd/harbor/root/labels/cmd.go +++ b/cmd/harbor/root/labels/cmd.go @@ -7,6 +7,12 @@ func Labels() *cobra.Command { Use: "label", Short: "Manage labels in Harbor", } + cmd.AddCommand( + CreateLabelCommand(), + DeleteLabelCommand(), + ListLabelCommand(), + UpdateLableCommand(), + ) return cmd } diff --git a/cmd/harbor/root/labels/create.go b/cmd/harbor/root/labels/create.go new file mode 100644 index 00000000..a336f1de --- /dev/null +++ b/cmd/harbor/root/labels/create.go @@ -0,0 +1,56 @@ +package labels + +import ( + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/views/label/create" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func CreateLabelCommand() *cobra.Command { + var opts create.CreateView + + cmd := &cobra.Command{ + Use: "create", + Short: "create label", + Long: "create label in harbor", + Example: "harbor label create", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + var err error + createView := &create.CreateView{ + Name: opts.Name, + Color: opts.Color, + Scope: opts.Scope, + Description: opts.Description, + } + if opts.Name != "" && opts.Scope != "" { + err = api.CreateLabel(opts) + } else { + err = createLabelView(createView) + } + + if err != nil { + log.Errorf("failed to create label: %v", err) + } + + }, + } + + flags := cmd.Flags() + flags.StringVarP(&opts.Name, "name", "n", "", "Name of the label") + flags.StringVarP(&opts.Color, "color", "", "#FFFFFF", "Color of the label.color is in hex value") + flags.StringVarP(&opts.Scope, "scope", "s", "g", "Scope of the label. eg- g(global), p(specific project)") + flags.StringVarP(&opts.Description, "description", "d", "", "Description of the label") + + return cmd +} + +func createLabelView(createView *create.CreateView) error { + if createView == nil { + createView = &create.CreateView{} + } + + create.CreateLabelView(createView) + return api.CreateLabel(*createView) +} diff --git a/cmd/harbor/root/labels/delete.go b/cmd/harbor/root/labels/delete.go index d8e69cdb..62d20d99 100644 --- a/cmd/harbor/root/labels/delete.go +++ b/cmd/harbor/root/labels/delete.go @@ -1 +1,40 @@ package labels + +import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func DeleteLabelCommand() *cobra.Command { + var opts models.Label + cmd := &cobra.Command{ + Use: "delete", + Short: "delete label", + Example: "harbor label delete [labelname]", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + deleteView := &api.ListFlags{ + Scope: opts.Scope, + } + + if len(args) > 0 { + labelId, _ := api.GetLabelIdByName(args[0]) + err = api.DeleteLabel(labelId) + } else { + labelId := prompt.GetLabelIdFromUser(*deleteView) + err = api.DeleteLabel(labelId) + } + if err != nil { + log.Errorf("failed to delete label: %v", err) + } + }, + } + flags := cmd.Flags() + flags.StringVarP(&opts.Scope, "scope", "s", "g", "default(global).'p' for project labels.Query scope of the label") + + return cmd +} diff --git a/cmd/harbor/root/labels/list.go b/cmd/harbor/root/labels/list.go new file mode 100644 index 00000000..be9c996d --- /dev/null +++ b/cmd/harbor/root/labels/list.go @@ -0,0 +1,41 @@ +package labels + +import ( + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/label/list" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func ListLabelCommand() *cobra.Command { + var opts api.ListFlags + + cmd := &cobra.Command{ + Use: "list", + Short: "list labels", + Run: func(cmd *cobra.Command, args []string) { + label, err := api.ListLabel(opts) + if err != nil { + log.Fatalf("failed to get label list: %v", err) + } + FormatFlag := viper.GetString("output-format") + if FormatFlag != "" { + utils.PrintPayloadInJSONFormat(label) + return + } + list.ListLabels(label.Payload) + }, + } + + flags := cmd.Flags() + flags.Int64VarP(&opts.Page, "page", "", 1, "Page number") + flags.Int64VarP(&opts.PageSize, "page-size", "", 20, "Size of per page") + flags.StringVarP(&opts.Q, "query", "q", "", "Query string to query resources") + flags.StringVarP(&opts.Scope, "scope", "s", "g", "default(global).'p' for project labels.Query scope of the label") + flags.Int64VarP(&opts.ProjectID, "projectid", "i", 1, "project ID when query project labels") + flags.StringVarP(&opts.Sort, "sort", "", "", "Sort the label list in ascending or descending order") + + return cmd +} diff --git a/cmd/harbor/root/labels/update.go b/cmd/harbor/root/labels/update.go new file mode 100644 index 00000000..5eab1e68 --- /dev/null +++ b/cmd/harbor/root/labels/update.go @@ -0,0 +1,76 @@ +package labels + +import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/api" + "github.com/goharbor/harbor-cli/pkg/prompt" + "github.com/goharbor/harbor-cli/pkg/views/label/update" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func UpdateLableCommand() *cobra.Command { + opts := &models.Label{} + + cmd := &cobra.Command{ + Use: "update", + Short: "update label", + Example: "harbor label update [labelname]", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + var labelId int64 + updateflags := api.ListFlags{ + Scope: opts.Scope, + } + + if len(args) > 0 { + labelId, err = api.GetLabelIdByName(args[0]) + } else { + labelId = prompt.GetLabelIdFromUser(updateflags) + } + if err != nil { + log.Errorf("failed to parse label id: %v", err) + } + + existingLabel := api.GetLabel(labelId) + if existingLabel == nil { + log.Errorf("label is not found") + return + } + updateView := &models.Label{ + Name: existingLabel.Name, + Color: existingLabel.Color, + Description: existingLabel.Description, + Scope: existingLabel.Scope, + } + + flags := cmd.Flags() + if flags.Changed("name") { + updateView.Name = opts.Name + } + if flags.Changed("color") { + updateView.Color = opts.Color + } + if flags.Changed("description") { + updateView.Description = opts.Description + } + if flags.Changed("scope") { + updateView.Scope = opts.Scope + } + + update.UpdateLabelView(updateView) + err = api.UpdateLabel(updateView, labelId) + if err != nil { + log.Errorf("failed to update label: %v", err) + } + }, + } + flags := cmd.Flags() + flags.StringVarP(&opts.Name, "name", "n", "", "Name of the label") + flags.StringVarP(&opts.Color, "color", "", "", "Color of the label.color is in hex value") + flags.StringVarP(&opts.Scope, "scope", "s", "g", "Scope of the label. eg- g(global), p(specific project)") + flags.StringVarP(&opts.Description, "description", "d", "", "Description of the label") + + return cmd +} diff --git a/cmd/harbor/root/registry/delete.go b/cmd/harbor/root/registry/delete.go index 92687744..7fbcfe1c 100644 --- a/cmd/harbor/root/registry/delete.go +++ b/cmd/harbor/root/registry/delete.go @@ -11,7 +11,7 @@ func DeleteRegistryCommand() *cobra.Command { cmd := &cobra.Command{ Use: "delete", - Short: "delete registry by id", + Short: "delete registry", Example: "harbor registry delete [registryname]", Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { diff --git a/pkg/api/label_handler.go b/pkg/api/label_handler.go new file mode 100644 index 00000000..30f4e60e --- /dev/null +++ b/pkg/api/label_handler.go @@ -0,0 +1,124 @@ +package api + +import ( + "fmt" + + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/label" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/label/create" +) + +func CreateLabel(opts create.CreateView) error { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return err + } + _, err = client.Label.CreateLabel(ctx, &label.CreateLabelParams{Label: &models.Label{Name: opts.Name, Color: opts.Color, Description: opts.Description, Scope: opts.Scope}}) + + if err != nil { + return err + } + + fmt.Printf("Label %s created\n", opts.Name) + return nil +} + +func DeleteLabel(Labelid int64) error { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return err + } + _, err = client.Label.DeleteLabel(ctx, &label.DeleteLabelParams{LabelID: Labelid}) + + if err != nil { + return err + } + + fmt.Println("label deleted successfully") + + return nil +} + +func ListLabel(opts ...ListFlags) (*label.ListLabelsOK, error) { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil, err + } + + var listFlags ListFlags + + if len(opts) > 0 { + listFlags = opts[0] + } + scope := "g" + response, err := client.Label.ListLabels(ctx, &label.ListLabelsParams{ + Page: &listFlags.Page, + PageSize: &listFlags.PageSize, + Q: &listFlags.Q, + Sort: &listFlags.Sort, + Scope: &scope, + ProjectID: &listFlags.ProjectID, + }) + + if err != nil { + return nil, err + } + + return response, nil +} + +func UpdateLabel(updateView *models.Label, Labelid int64) error { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return err + } + labelUpdate := &models.Label{ + Name: updateView.Name, + Color: updateView.Color, + Description: updateView.Description, + Scope: updateView.Scope, + } + + _, err = client.Label.UpdateLabel( + ctx, + &label.UpdateLabelParams{LabelID: Labelid, Label: labelUpdate}, + ) + if err != nil { + return err + } + + fmt.Println("label updated successfully") + + return nil +} + +func GetLabel(labelid int64) *models.Label { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil + } + response, err := client.Label.GetLabelByID(ctx, &label.GetLabelByIDParams{LabelID: labelid}) + if err != nil { + return nil + } + + return response.GetPayload() +} + +func GetLabelIdByName(labelName string) (int64, error) { + var opts ListFlags + + l, err := ListLabel(opts) + if err != nil { + return 0, err + } + + for _, label := range l.Payload { + if label.Name == labelName { + return label.ID, nil + } + } + + return 0, err +} diff --git a/pkg/api/types.go b/pkg/api/types.go index 5d69b032..018a6f76 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1,12 +1,14 @@ package api type ListFlags struct { - Name string - Page int64 - PageSize int64 - Q string - Sort string - Public bool + ProjectID int64 + Scope string + Name string + Page int64 + PageSize int64 + Q string + Sort string + Public bool } // CreateView for Registry diff --git a/pkg/prompt/prompt.go b/pkg/prompt/prompt.go index ccf96a5c..5f46e952 100644 --- a/pkg/prompt/prompt.go +++ b/pkg/prompt/prompt.go @@ -4,6 +4,7 @@ import ( "github.com/goharbor/harbor-cli/pkg/api" aview "github.com/goharbor/harbor-cli/pkg/views/artifact/select" tview "github.com/goharbor/harbor-cli/pkg/views/artifact/tags/select" + lview "github.com/goharbor/harbor-cli/pkg/views/label/select" pview "github.com/goharbor/harbor-cli/pkg/views/project/select" rview "github.com/goharbor/harbor-cli/pkg/views/registry/select" repoView "github.com/goharbor/harbor-cli/pkg/views/repository/select" @@ -89,3 +90,13 @@ func GetTagNameFromUser() string { return <-repoName } + +func GetLabelIdFromUser(opts api.ListFlags) int64 { + labelId := make(chan int64) + go func() { + response, _ := api.ListLabel(opts) + lview.LabelList(response.Payload, labelId) + }() + + return <-labelId +} diff --git a/pkg/views/label/create/view.go b/pkg/views/label/create/view.go new file mode 100644 index 00000000..fba21e12 --- /dev/null +++ b/pkg/views/label/create/view.go @@ -0,0 +1,82 @@ +package create + +import ( + "errors" + + "github.com/charmbracelet/huh" + log "github.com/sirupsen/logrus" +) + +type CreateView struct { + Name string + Color string + Description string + Scope string +} + +func CreateLabelView(createView *CreateView) { + theme := huh.ThemeCharm() + err := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Name"). + Value(&createView.Name). + Validate(func(str string) error { + if str == "" { + return errors.New("name cannot be empty") + } + return nil + }), + huh.NewSelect[string](). + Title("Color"). + Options( + huh.NewOption("White", "#FFFFFF"), + huh.NewOption("Black", "#000000"), + huh.NewOption("Jet Grey", "#61717D"), + huh.NewOption("Grey", "#737373"), + huh.NewOption("Spicy Pink", "#80746D"), + huh.NewOption("Cadet Blue", "#A9B6BE"), + huh.NewOption("Alto", "#DDDDDD"), + huh.NewOption("Silk", "#BBB3A9"), + huh.NewOption("Endeavour", "#0065AB"), + huh.NewOption("Sapphire", "#343DAC"), + huh.NewOption("Violet", "#781DA0"), + huh.NewOption("Jazzberry Jam", "#9B0D54"), + huh.NewOption("Blue", "#0095D3"), + huh.NewOption("Purple", "#9DA3DB"), + huh.NewOption("Bright Lavender", "#BE90D6"), + huh.NewOption("Rose", "#F1428A"), + huh.NewOption("Navy Green", "#1D5100"), + huh.NewOption("Dark Aqua", "#006668"), + huh.NewOption("Peacock Blue", "#006690"), + huh.NewOption("Regal Blue", "#004A70"), + huh.NewOption("Green", "#48960C"), + huh.NewOption("Cyan", "#00AB9A"), + huh.NewOption("Cerulean", "#00B7D6"), + huh.NewOption("Nice Blue", "#0081A7"), + huh.NewOption("Red", "#C92100"), + huh.NewOption("Thunderbird", "#CD3517"), + huh.NewOption("Rust Orange", "#C25400"), + huh.NewOption("Yellow Brown", "#D28F00"), + huh.NewOption("Radical Red", "#F52F52"), + huh.NewOption("Reddish Orange", "#FF5501"), + huh.NewOption("Orange", "#F57600"), + huh.NewOption("Yellow", "#FFDC0B"), + ). + Value(&createView.Color). + Validate(func(str string) error { + if str == "" { + return errors.New("color cannot be empty") + } + return nil + }), + huh.NewInput(). + Title("Description"). + Value(&createView.Description), + ), + ).WithTheme(theme).Run() + + if err != nil { + log.Fatal(err) + } +} diff --git a/pkg/views/label/list/view.go b/pkg/views/label/list/view.go new file mode 100644 index 00000000..169eee6b --- /dev/null +++ b/pkg/views/label/list/view.go @@ -0,0 +1,40 @@ +package list + +import ( + "fmt" + "os" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/goharbor/harbor-cli/pkg/views/base/tablelist" +) + +var columns = []table.Column{ + {Title: "ID", Width: 6}, + {Title: "Name", Width: 12}, + {Title: "Color", Width: 12}, + {Title: "Description", Width: 18}, + {Title: "Creation Time", Width: 24}, +} + +func ListLabels(labels []*models.Label) { + var rows []table.Row + for _, regis := range labels { + createdTime, _ := utils.FormatCreatedTime(regis.CreationTime.String()) + rows = append(rows, table.Row{ + fmt.Sprintf("%d", regis.ID), + regis.Name, + regis.Color, + regis.Description, + createdTime, + }) + } + + m := tablelist.NewModel(columns, rows, len(rows)) + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/pkg/views/label/select/view.go b/pkg/views/label/select/view.go new file mode 100644 index 00000000..f511b694 --- /dev/null +++ b/pkg/views/label/select/view.go @@ -0,0 +1,36 @@ +package delete + +import ( + "fmt" + "os" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + "github.com/goharbor/harbor-cli/pkg/views/base/selection" +) + +func LabelList(label []*models.Label, choice chan<- int64) { + itemsList := make([]list.Item, len(label)) + + items := map[string]int64{} + + for i, m := range label { + items[m.Name] = m.ID + itemsList[i] = selection.Item(m.Name) + } + + m := selection.NewModel(itemsList, "Label") + + p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() + + if err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } + + if p, ok := p.(selection.Model); ok { + choice <- items[p.Choice] + } + +} diff --git a/pkg/views/label/update/view.go b/pkg/views/label/update/view.go new file mode 100644 index 00000000..63bc3503 --- /dev/null +++ b/pkg/views/label/update/view.go @@ -0,0 +1,76 @@ +package update + +import ( + "errors" + + "github.com/charmbracelet/huh" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + log "github.com/sirupsen/logrus" +) + +func UpdateLabelView(updateView *models.Label) { + theme := huh.ThemeCharm() + err := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Name"). + Value(&updateView.Name). + Validate(func(str string) error { + if str == "" { + return errors.New("name cannot be empty") + } + return nil + }), + huh.NewSelect[string](). + Title("Color"). + Options( + huh.NewOption("White", "#FFFFFF"), + huh.NewOption("Black", "#000000"), + huh.NewOption("Jet Grey", "#61717D"), + huh.NewOption("Grey", "#737373"), + huh.NewOption("Spicy Pink", "#80746D"), + huh.NewOption("Cadet Blue", "#A9B6BE"), + huh.NewOption("Alto", "#DDDDDD"), + huh.NewOption("Silk", "#BBB3A9"), + huh.NewOption("Endeavour", "#0065AB"), + huh.NewOption("Sapphire", "#343DAC"), + huh.NewOption("Violet", "#781DA0"), + huh.NewOption("Jazzberry Jam", "#9B0D54"), + huh.NewOption("Blue", "#0095D3"), + huh.NewOption("Purple", "#9DA3DB"), + huh.NewOption("Bright Lavender", "#BE90D6"), + huh.NewOption("Rose", "#F1428A"), + huh.NewOption("Navy Green", "#1D5100"), + huh.NewOption("Dark Aqua", "#006668"), + huh.NewOption("Peacock Blue", "#006690"), + huh.NewOption("Regal Blue", "#004A70"), + huh.NewOption("Green", "#48960C"), + huh.NewOption("Cyan", "#00AB9A"), + huh.NewOption("Cerulean", "#00B7D6"), + huh.NewOption("Nice Blue", "#0081A7"), + huh.NewOption("Red", "#C92100"), + huh.NewOption("Thunderbird", "#CD3517"), + huh.NewOption("Rust Orange", "#C25400"), + huh.NewOption("Yellow Brown", "#D28F00"), + huh.NewOption("Radical Red", "#F52F52"), + huh.NewOption("Reddish Orange", "#FF5501"), + huh.NewOption("Orange", "#F57600"), + huh.NewOption("Yellow", "#FFDC0B"), + ). + Value(&updateView.Color). + Validate(func(str string) error { + if str == "" { + return errors.New("color cannot be empty") + } + return nil + }), + huh.NewInput(). + Title("Description"). + Value(&updateView.Description), + ), + ).WithTheme(theme).Run() + + if err != nil { + log.Fatal(err) + } +} From 9d440ebedc6c68d8a2135b8f6655499eea78e2d1 Mon Sep 17 00:00:00 2001 From: ALTHAF <114910365+Althaf66@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:26:46 +0530 Subject: [PATCH 28/41] feat: modified registy update command (#88) * modified registy update command Signed-off-by: Althaf66 * modified registry cmd Signed-off-by: ALTHAF * modified registry cmd Signed-off-by: ALTHAF --------- Signed-off-by: Althaf66 Signed-off-by: ALTHAF <114910365+Althaf66@users.noreply.github.com> Signed-off-by: ALTHAF Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/registry/update.go | 131 ++++++++++++++--------------- pkg/api/registry_handler.go | 18 +++- pkg/views/registry/update/view.go | 64 ++++++++++++++ 3 files changed, 145 insertions(+), 68 deletions(-) create mode 100644 pkg/views/registry/update/view.go diff --git a/cmd/harbor/root/registry/update.go b/cmd/harbor/root/registry/update.go index 0db5db45..84b17aec 100644 --- a/cmd/harbor/root/registry/update.go +++ b/cmd/harbor/root/registry/update.go @@ -1,103 +1,100 @@ package registry import ( + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" "github.com/goharbor/harbor-cli/pkg/api" "github.com/goharbor/harbor-cli/pkg/prompt" - "github.com/goharbor/harbor-cli/pkg/views/registry/create" + "github.com/goharbor/harbor-cli/pkg/views/registry/update" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -// NewUpdateRegistryCommand creates a new `harbor update registry` command func UpdateRegistryCommand() *cobra.Command { - var opts api.CreateRegView + opts := &models.Registry{ + Credential: &models.RegistryCredential{}, + } cmd := &cobra.Command{ - Use: "update", - Short: "update registry", - Example: "harbor registry update [registryname]", - Args: cobra.MaximumNArgs(1), + Use: "update [registry_name]", + Short: "update registry", + Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error var registryId int64 - updateView := &api.CreateRegView{ - Name: opts.Name, - Type: opts.Type, - Description: opts.Description, - URL: opts.URL, - Credential: api.RegistryCredential{ - AccessKey: opts.Credential.AccessKey, - Type: opts.Credential.Type, - AccessSecret: opts.Credential.AccessSecret, - }, - Insecure: opts.Insecure, - } - if len(args) > 0 { registryId, err = api.GetRegistryIdByName(args[0]) + if err != nil { + log.Errorf("failed to get registry id: %v", err) + return + } } else { registryId = prompt.GetRegistryNameFromUser() } - if err != nil { - log.Errorf("failed to parse registry id: %v", err) + existingRegistry := api.GetRegistryResponse(registryId) + if existingRegistry == nil { + log.Errorf("registry is not found") + return } - if opts.Name != "" && opts.Type != "" && opts.URL != "" { - err = api.UpdateRegistry(updateView, registryId) - } else { - err = updateRegistryView(updateView, registryId) + updateView := &models.Registry{ + Name: existingRegistry.Name, + Type: existingRegistry.Type, + Description: existingRegistry.Description, + URL: existingRegistry.URL, + Insecure: existingRegistry.Insecure, + Credential: &models.RegistryCredential{ + AccessKey: existingRegistry.Credential.AccessKey, + AccessSecret: existingRegistry.Credential.AccessSecret, + Type: existingRegistry.Credential.Type, + }, + } + + flags := cmd.Flags() + if flags.Changed("name") { + updateView.Name = opts.Name + } + if flags.Changed("type") { + updateView.Type = opts.Type + } + if flags.Changed("description") { + updateView.Description = opts.Description + } + if flags.Changed("url") { + updateView.URL = opts.URL + } + if flags.Changed("insecure") { + updateView.Insecure = opts.Insecure + } + if flags.Changed("credential-access-key") { + updateView.Credential.AccessKey = opts.Credential.AccessKey + } + if flags.Changed("credential-access-secret") { + updateView.Credential.AccessSecret = opts.Credential.AccessSecret + } + if flags.Changed("credential-type") { + updateView.Credential.Type = opts.Credential.Type } + update.UpdateRegistryView(updateView) + err = api.UpdateRegistry(updateView, registryId) if err != nil { log.Errorf("failed to update registry: %v", err) + return } }, } flags := cmd.Flags() - flags.StringVarP(&opts.Name, "name", "", "", "Name of the registry") - flags.StringVarP(&opts.Type, "type", "", "", "Type of the registry") - flags.StringVarP(&opts.URL, "url", "", "", "Registry endpoint URL") - flags.StringVarP(&opts.Description, "description", "", "", "Description of the registry") - flags.BoolVarP( - &opts.Insecure, - "insecure", - "", - true, - "Whether or not the certificate will be verified when Harbor tries to access the server", - ) - flags.StringVarP( - &opts.Credential.AccessKey, - "credential-access-key", - "", - "", - "Access key, e.g. user name when credential type is 'basic'", - ) - flags.StringVarP( - &opts.Credential.AccessSecret, - "credential-access-secret", - "", - "", - "Access secret, e.g. password when credential type is 'basic'", - ) - flags.StringVarP( - &opts.Credential.Type, - "credential-type", - "", - "", - "Credential type, such as 'basic', 'oauth'", - ) + flags.StringVarP(&opts.Name, "name", "n", "", "Name of the registry") + flags.StringVarP(&opts.Type, "type", "t", "", "Type of the registry") + flags.StringVarP(&opts.URL, "url", "u", "", "Registry endpoint URL") + flags.StringVarP(&opts.Description, "description", "d", "", "Description of the registry") + flags.BoolVarP(&opts.Insecure, "insecure", "i", false, "Whether or not the certificate will be verified when Harbor tries to access the server") + flags.StringVarP(&opts.Credential.AccessKey, "credential-access-key", "k", "", "Access key, e.g. user name when credential type is 'basic'") + flags.StringVarP(&opts.Credential.AccessSecret, "credential-access-secret", "s", "", "Access secret, e.g. password when credential type is 'basic'") + flags.StringVarP(&opts.Credential.Type, "credential-type", "c", "", "Credential type, such as 'basic', 'oauth'") return cmd } - -func updateRegistryView(updateView *api.CreateRegView, projectID int64) error { - if updateView == nil { - updateView = &api.CreateRegView{} - } - - create.CreateRegistryView(updateView) - return api.UpdateRegistry(updateView, projectID) -} diff --git a/pkg/api/registry_handler.go b/pkg/api/registry_handler.go index 0df427fc..490fe08a 100644 --- a/pkg/api/registry_handler.go +++ b/pkg/api/registry_handler.go @@ -100,7 +100,23 @@ func ViewRegistry(registryId int64) (*registry.GetRegistryOK, error) { return response, nil } -func UpdateRegistry(updateView *CreateRegView, projectID int64) error { +func GetRegistryResponse(registryId int64) *models.Registry { + ctx, client, err := utils.ContextWithClient() + if err != nil { + return nil + } + response, err := client.Registry.GetRegistry(ctx, ®istry.GetRegistryParams{ID: registryId}) + if err != nil { + return nil + } + if response.Payload.ID == 0 { + return nil + } + + return response.GetPayload() +} + +func UpdateRegistry(updateView *models.Registry, projectID int64) error { ctx, client, err := utils.ContextWithClient() if err != nil { return err diff --git a/pkg/views/registry/update/view.go b/pkg/views/registry/update/view.go new file mode 100644 index 00000000..c073c400 --- /dev/null +++ b/pkg/views/registry/update/view.go @@ -0,0 +1,64 @@ +package update + +import ( + "errors" + + "github.com/charmbracelet/huh" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" + log "github.com/sirupsen/logrus" +) + +func UpdateRegistryView(updateView *models.Registry) { + theme := huh.ThemeCharm() + err := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Provider"). + Value(&updateView.Type). + Validate(func(str string) error { + if str == "" { + return errors.New("provider cannot be empty") + } + return nil + }), + huh.NewInput(). + Title("Name"). + Value(&updateView.Name). + Validate(func(str string) error { + if str == "" { + return errors.New("name cannot be empty") + } + return nil + }), + huh.NewInput(). + Title("Description"). + Value(&updateView.Description), + huh.NewInput(). + Title("URL"). + Value(&updateView.URL). + Validate(func(str string) error { + if str == "" { + return errors.New("url cannot be empty") + } + return nil + }), + huh.NewInput(). + Title("Access ID"). + Value(&updateView.Credential.AccessKey), + huh.NewInput(). + Title("Access Secret"). + EchoMode(huh.EchoModePassword). + Description("Replace the Access Secret to the real one"). + Value(&updateView.Credential.AccessSecret), + huh.NewConfirm(). + Title("Verify Cert"). + Value(&updateView.Insecure). + Affirmative("yes"). + Negative("no"), + ), + ).WithTheme(theme).Run() + + if err != nil { + log.Fatal(err) + } +} From 2f936b0b8e5fdf536ebfcbfcf27550e057b93de1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:51:39 -0700 Subject: [PATCH 29/41] build(deps): bump github.com/charmbracelet/huh from 0.5.2 to 0.6.0 (#217) Signed-off-by: Patrick Eschenbach --- go.mod | 4 ++-- go.sum | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 69340c72..14840ae3 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.5 require ( github.com/charmbracelet/bubbles v0.20.0 github.com/charmbracelet/bubbletea v1.1.0 - github.com/charmbracelet/huh v0.5.2 + github.com/charmbracelet/huh v0.6.0 github.com/charmbracelet/lipgloss v0.13.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 @@ -41,7 +41,7 @@ require ( github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.15.2 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect diff --git a/go.sum b/go.sum index a544c81d..5abe29b6 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQW github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= -github.com/charmbracelet/huh v0.5.2 h1:ofeNkJ4iaFnzv46Njhx896DzLUe/j0L2QAf8znwzX4c= -github.com/charmbracelet/huh v0.5.2/go.mod h1:Sf7dY0oAn6N/e3sXJFtFX9hdQLrUdO3z7AYollG9bAM= +github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= +github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= @@ -26,8 +26,6 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAM github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/strings v0.0.0-20240725160154-f9f6568126ec h1:G79PnmxvVeo5FbAip3VPqAaBkOeXExwvhIBUfrXD5MA= github.com/charmbracelet/x/exp/strings v0.0.0-20240725160154-f9f6568126ec/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= -github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE= -github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -110,8 +108,8 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= From 3ddbf3dc30110fcddb68ca1d3af6288079e850ba Mon Sep 17 00:00:00 2001 From: Alan Tang Date: Sat, 21 Dec 2024 05:52:04 +0800 Subject: [PATCH 30/41] feature: Support YAML output for newly added commands (#268) Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/artifact/list.go | 1 + cmd/harbor/root/artifact/view.go | 1 + cmd/harbor/root/labels/list.go | 9 ++++++--- cmd/harbor/root/project/list.go | 1 + cmd/harbor/root/project/logs.go | 1 + cmd/harbor/root/project/search.go | 10 +++++++--- cmd/harbor/root/registry/list.go | 1 + cmd/harbor/root/repository/list.go | 1 + cmd/harbor/root/repository/search.go | 11 ++++++++--- cmd/harbor/root/schedule/list.go | 13 +++++++++---- 10 files changed, 36 insertions(+), 13 deletions(-) diff --git a/cmd/harbor/root/artifact/list.go b/cmd/harbor/root/artifact/list.go index c378f05e..d343faf7 100644 --- a/cmd/harbor/root/artifact/list.go +++ b/cmd/harbor/root/artifact/list.go @@ -34,6 +34,7 @@ func ListArtifactCommand() *cobra.Command { if err != nil { log.Errorf("failed to list artifacts: %v", err) + return } FormatFlag := viper.GetString("output-format") diff --git a/cmd/harbor/root/artifact/view.go b/cmd/harbor/root/artifact/view.go index 7d13c0f6..a16531b0 100644 --- a/cmd/harbor/root/artifact/view.go +++ b/cmd/harbor/root/artifact/view.go @@ -35,6 +35,7 @@ func ViewArtifactCommmand() *cobra.Command { if err != nil { log.Errorf("failed to get info of an artifact: %v", err) + return } FormatFlag := viper.GetString("output-format") diff --git a/cmd/harbor/root/labels/list.go b/cmd/harbor/root/labels/list.go index be9c996d..84f127bb 100644 --- a/cmd/harbor/root/labels/list.go +++ b/cmd/harbor/root/labels/list.go @@ -22,10 +22,13 @@ func ListLabelCommand() *cobra.Command { } FormatFlag := viper.GetString("output-format") if FormatFlag != "" { - utils.PrintPayloadInJSONFormat(label) - return + err = utils.PrintFormat(label, FormatFlag) + if err != nil { + log.Error(err) + } + } else { + list.ListLabels(label.Payload) } - list.ListLabels(label.Payload) }, } diff --git a/cmd/harbor/root/project/list.go b/cmd/harbor/root/project/list.go index 3a908b42..9fae2bf4 100644 --- a/cmd/harbor/root/project/list.go +++ b/cmd/harbor/root/project/list.go @@ -34,6 +34,7 @@ func ListProjectCommand() *cobra.Command { if err != nil { log.Fatalf("failed to get projects list: %v", err) + return } FormatFlag := viper.GetString("output-format") if FormatFlag != "" { diff --git a/cmd/harbor/root/project/logs.go b/cmd/harbor/root/project/logs.go index 8e4cac54..b333cb23 100644 --- a/cmd/harbor/root/project/logs.go +++ b/cmd/harbor/root/project/logs.go @@ -29,6 +29,7 @@ func LogsProjectCommmand() *cobra.Command { if err != nil { log.Fatalf("failed to get project logs: %v", err) + return } FormatFlag := viper.GetString("output-format") diff --git a/cmd/harbor/root/project/search.go b/cmd/harbor/root/project/search.go index 89ed7a67..0e1ce12a 100644 --- a/cmd/harbor/root/project/search.go +++ b/cmd/harbor/root/project/search.go @@ -20,13 +20,17 @@ func SearchProjectCommand() *cobra.Command { if err != nil { log.Fatalf("failed to get projects: %v", err) } + FormatFlag := viper.GetString("output-format") if FormatFlag != "" { - utils.PrintPayloadInJSONFormat(projects) - return + err = utils.PrintFormat(projects, FormatFlag) + if err != nil { + log.Error(err) + } + } else { + list.SearchProjects(projects.Payload.Project) } - list.SearchProjects(projects.Payload.Project) }, } return cmd diff --git a/cmd/harbor/root/registry/list.go b/cmd/harbor/root/registry/list.go index 78598511..c55036f6 100644 --- a/cmd/harbor/root/registry/list.go +++ b/cmd/harbor/root/registry/list.go @@ -21,6 +21,7 @@ func ListRegistryCommand() *cobra.Command { if err != nil { log.Fatalf("failed to get projects list: %v", err) + return } FormatFlag := viper.GetString("output-format") if FormatFlag != "" { diff --git a/cmd/harbor/root/repository/list.go b/cmd/harbor/root/repository/list.go index 41ca0492..f960d5e2 100644 --- a/cmd/harbor/root/repository/list.go +++ b/cmd/harbor/root/repository/list.go @@ -33,6 +33,7 @@ func ListRepositoryCommand() *cobra.Command { if err != nil { log.Errorf("failed to list repositories: %v", err) + return } FormatFlag := viper.GetString("output-format") diff --git a/cmd/harbor/root/repository/search.go b/cmd/harbor/root/repository/search.go index 9463741d..3071a813 100644 --- a/cmd/harbor/root/repository/search.go +++ b/cmd/harbor/root/repository/search.go @@ -19,14 +19,19 @@ func SearchRepoCmd() *cobra.Command { repo, err := api.SearchRepository(args[0]) if err != nil { log.Fatalf("failed to get repositories: %v", err) + return } + FormatFlag := viper.GetString("output-format") if FormatFlag != "" { - utils.PrintPayloadInJSONFormat(repo) - return + err = utils.PrintFormat(repo, FormatFlag) + if err != nil { + log.Error(err) + } + } else { + search.SearchRepositories(repo.Payload.Repository) } - search.SearchRepositories(repo.Payload.Repository) }, } return cmd diff --git a/cmd/harbor/root/schedule/list.go b/cmd/harbor/root/schedule/list.go index 91522054..88895fd2 100644 --- a/cmd/harbor/root/schedule/list.go +++ b/cmd/harbor/root/schedule/list.go @@ -17,16 +17,21 @@ func ListScheduleCommand() *cobra.Command { Short: "show all schedule jobs in Harbor", Run: func(cmd *cobra.Command, args []string) { schedule, err := api.ListSchedule(opts) + if err != nil { log.Fatalf("failed to get schedule list: %v", err) + return } + FormatFlag := viper.GetString("output-format") if FormatFlag != "" { - utils.PrintPayloadInJSONFormat(schedule) - return + err = utils.PrintFormat(schedule, FormatFlag) + if err != nil { + log.Error(err) + } + } else { + list.ListSchedule(schedule.Payload) } - - list.ListSchedule(schedule.Payload) }, } From 77f3a44313ac59d5acaf9f6b052ad76554ef2031 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Sun, 22 Dec 2024 13:38:28 +0100 Subject: [PATCH 31/41] Added error propagation to config sub commands; Added tests for subcommands Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/config/delete.go | 20 ++-- cmd/harbor/root/config/get.go | 30 +++-- cmd/harbor/root/config/update.go | 19 +-- test/e2e/config_cmd_test.go | 192 +++++++++++++++++++++++++++++++ 4 files changed, 229 insertions(+), 32 deletions(-) create mode 100644 test/e2e/config_cmd_test.go diff --git a/cmd/harbor/root/config/delete.go b/cmd/harbor/root/config/delete.go index fddb7190..5bb8a77e 100644 --- a/cmd/harbor/root/config/delete.go +++ b/cmd/harbor/root/config/delete.go @@ -20,12 +20,14 @@ func DeleteConfigItemCommand() *cobra.Command { Long: `Clear the value of a specific CLI config item by setting it to its zero value. Case-insensitive field lookup, but uses the canonical (Go) field name internally.`, Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { + + // Switch from Run to RunE + RunE: func(cmd *cobra.Command, args []string) error { // 1. Load the current config config, err := utils.GetCurrentHarborConfig() if err != nil { - logrus.Errorf("Failed to load Harbor config: %v", err) - return + // Return the error so it's propagated + return fmt.Errorf("failed to load Harbor config: %w", err) } // 2. Parse the user-supplied item path (e.g., "credentials.password") @@ -34,19 +36,21 @@ Case-insensitive field lookup, but uses the canonical (Go) field name internally // 3. Reflection-based delete (zero out) actualSegments := []string{} if err := deleteValueInConfig(config, itemPath, &actualSegments); err != nil { - logrus.Error(err) - return + // Return the error for propagation + return fmt.Errorf("failed to delete value in config: %w", err) } // 4. Persist the updated config to disk if err := utils.UpdateConfigFile(config); err != nil { - logrus.Errorf("Failed to save updated config: %v", err) - return + return fmt.Errorf("failed to save updated config: %w", err) } - // 5. Confirm to the user + // 5. Confirm to the user (no error here) canonicalPath := strings.Join(actualSegments, ".") logrus.Infof("Successfully cleared %s", canonicalPath) + + // If everything succeeds, return nil + return nil }, } diff --git a/cmd/harbor/root/config/get.go b/cmd/harbor/root/config/get.go index 743fece9..4f9137f3 100644 --- a/cmd/harbor/root/config/get.go +++ b/cmd/harbor/root/config/get.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/goharbor/harbor-cli/pkg/utils" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" "gopkg.in/yaml.v3" @@ -21,55 +20,54 @@ func GetConfigItemCommand() *cobra.Command { Example: ` harbor config get credentials.username`, Long: `Get the value of a specific CLI config item`, Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { // 1. Load config config, err := utils.GetCurrentHarborConfig() if err != nil { - logrus.Errorf("Failed to get config: %v", err) - return + // Return an error rather than just logging. + return fmt.Errorf("failed to get config: %w", err) } // 2. Parse the user-supplied item path (e.g. "credentials.username") itemPath := strings.Split(args[0], ".") - // 3. Get the value from the config (filtering credentials if needed) - // also capture the actual field name segments for pretty output + // 3. Get the value from the config (and track actual field segments for output) actualSegments := []string{} result, err := getValueFromConfig(config, itemPath, &actualSegments) if err != nil { - logrus.Error(err) - return + // Return the error so it propagates to the caller/test. + return err } - // 4. Prepare the final output as a map so we can render easily in JSON/YAML - // We join the actual field names with "." for the "canonical" path. + // 4. Prepare the final output as a map so we can render easily in JSON/YAML. canonicalPath := strings.Join(actualSegments, ".") output := map[string]interface{}{ canonicalPath: result, } - // 5. Determine output format + // 5. Determine the output format (json, yaml, etc.) and print. formatFlag := viper.GetString("output-format") switch formatFlag { case "json": data, err := json.MarshalIndent(output, "", " ") if err != nil { - logrus.Errorf("Failed to marshal output to JSON: %v", err) - return + return fmt.Errorf("failed to marshal output to JSON: %w", err) } fmt.Println(string(data)) case "yaml", "": data, err := yaml.Marshal(output) if err != nil { - logrus.Errorf("Failed to marshal output to YAML: %v", err) - return + return fmt.Errorf("failed to marshal output to YAML: %w", err) } fmt.Println(string(data)) default: - logrus.Errorf("Unsupported output format: %s", formatFlag) + return fmt.Errorf("unsupported output format: %s", formatFlag) } + + // If everything succeeds, return nil. + return nil }, } diff --git a/cmd/harbor/root/config/update.go b/cmd/harbor/root/config/update.go index adeec1ba..71a5a50b 100644 --- a/cmd/harbor/root/config/update.go +++ b/cmd/harbor/root/config/update.go @@ -21,12 +21,14 @@ func SetConfigItemCommand() *cobra.Command { Long: `Set the value of a specific CLI config item. Case-insensitive field lookup, but uses the canonical (Go) field name internally.`, Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { + + // Switch from Run to RunE so we can propagate errors + RunE: func(cmd *cobra.Command, args []string) error { // 1. Load the current config config, err := utils.GetCurrentHarborConfig() if err != nil { - logrus.Errorf("Failed to load Harbor config: %v", err) - return + // Return the error (with context) instead of just logging + return fmt.Errorf("failed to load Harbor config: %w", err) } // 2. Parse the user-supplied item path (e.g., "credentials.password") @@ -36,19 +38,20 @@ Case-insensitive field lookup, but uses the canonical (Go) field name internally // 3. Reflection-based set actualSegments := []string{} if err := setValueInConfig(config, itemPath, newValue, &actualSegments); err != nil { - logrus.Error(err) - return + return fmt.Errorf("failed to set value in config: %w", err) } // 4. Persist the updated config to disk if err := utils.UpdateConfigFile(config); err != nil { - logrus.Errorf("Failed to save updated config: %v", err) - return + return fmt.Errorf("failed to save updated config: %w", err) } - // 5. Confirm to the user + // 5. Confirm to the user (logrus.Info is fine here; no error) canonicalPath := strings.Join(actualSegments, ".") logrus.Infof("Successfully updated %s to '%s'", canonicalPath, newValue) + + // If everything is fine, return nil + return nil }, } diff --git a/test/e2e/config_cmd_test.go b/test/e2e/config_cmd_test.go new file mode 100644 index 00000000..38a3838c --- /dev/null +++ b/test/e2e/config_cmd_test.go @@ -0,0 +1,192 @@ +package e2e + +import ( + "testing" + + "github.com/goharbor/harbor-cli/cmd/harbor/root" + "github.com/goharbor/harbor-cli/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func Test_ConfigCmd(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config"}) + err := rootCmd.Execute() + assert.Nil(t, err) +} + +func Test_ConfigListCmd(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "list"}) + err := rootCmd.Execute() + assert.Nil(t, err) +} + +func Test_ConfigGetCmd_Success(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + testConfig := &utils.HarborConfig{ + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []utils.Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, + } + err := utils.UpdateConfigFile(testConfig) + if err != nil { + t.Fatal(err) + } + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "get", "credentials.serveraddress"}) + err = rootCmd.Execute() + assert.NoError(t, err) +} + +func Test_ConfigGetCmd_Failure(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + testConfig := &utils.HarborConfig{ + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []utils.Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, + } + err := utils.UpdateConfigFile(testConfig) + if err != nil { + t.Fatal(err) + } + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "get", "serveraddress"}) + err = rootCmd.Execute() + assert.Error(t, err, "Expected an error when getting a non-existent config item") +} + +func Test_ConfigSetCmd_Success(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + testConfig := &utils.HarborConfig{ + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []utils.Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, + } + err := utils.UpdateConfigFile(testConfig) + if err != nil { + t.Fatal(err) + } + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "set", "credentials.serveraddress", "http://demo.goharbor.io"}) + err = rootCmd.Execute() + assert.NoError(t, err) +} + +func Test_ConfigSetCmd_Failure(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + testConfig := &utils.HarborConfig{ + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []utils.Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, + } + err := utils.UpdateConfigFile(testConfig) + if err != nil { + t.Fatal(err) + } + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "set", "serveraddress", "http://demo.goharbor.io"}) + err = rootCmd.Execute() + assert.Error(t, err, "Expected an error when setting a non-existent config item") +} + +func Test_ConfigDeleteCmd_Success(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + testConfig := &utils.HarborConfig{ + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []utils.Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, + } + err := utils.UpdateConfigFile(testConfig) + if err != nil { + t.Fatal(err) + } + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "delete", "credentials.serveraddress"}) + err = rootCmd.Execute() + assert.NoError(t, err) + config, err := utils.GetCurrentHarborConfig() + if err != nil { + t.Fatal(err) + } + assert.Empty(t, config.Credentials[0].ServerAddress) +} + +func Test_ConfigDeleteCmd_Failure(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + testConfig := &utils.HarborConfig{ + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []utils.Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, + } + err := utils.UpdateConfigFile(testConfig) + if err != nil { + t.Fatal(err) + } + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "delete", "serveraddress"}) + err = rootCmd.Execute() + assert.Error(t, err, "Expected an error when deleting a non-existent config item") +} From 98eae89dbb557de6afda256cb6bce346643b1665 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Sun, 22 Dec 2024 14:00:30 +0100 Subject: [PATCH 32/41] Added --name flag for credential selection; Added subcommand tests Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/config/delete.go | 74 +++++++++----- cmd/harbor/root/config/get.go | 79 +++++++++------ cmd/harbor/root/config/update.go | 68 +++++++++---- pkg/utils/config.go | 11 ++- test/e2e/config_cmd_test.go | 161 +++++++++++++++++++++++++++++++ 5 files changed, 322 insertions(+), 71 deletions(-) diff --git a/cmd/harbor/root/config/delete.go b/cmd/harbor/root/config/delete.go index 5bb8a77e..ad61c15b 100644 --- a/cmd/harbor/root/config/delete.go +++ b/cmd/harbor/root/config/delete.go @@ -13,20 +13,28 @@ import ( // DeleteConfigItemCommand creates the 'harbor config delete' subcommand, // allowing you to do: harbor config delete func DeleteConfigItemCommand() *cobra.Command { + var credentialName string + cmd := &cobra.Command{ - Use: "delete ", - Short: "Delete (clear) a specific config item", - Example: " harbor config delete credentials.password", + Use: "delete ", + Short: "Delete (clear) a specific config item", + Example: ` + # Clear the current credential's password + harbor config delete credentials.password + + # Clear a specific credential's password using --name + harbor config delete credentials.password --name harbor-cli@http://demo.goharbor.io +`, Long: `Clear the value of a specific CLI config item by setting it to its zero value. -Case-insensitive field lookup, but uses the canonical (Go) field name internally.`, +Case-insensitive field lookup, but uses the canonical (Go) field name internally. +If you specify --name, that credential (rather than the "current" one) will be used.`, Args: cobra.ExactArgs(1), - // Switch from Run to RunE + // Use RunE so we can propagate errors RunE: func(cmd *cobra.Command, args []string) error { // 1. Load the current config config, err := utils.GetCurrentHarborConfig() if err != nil { - // Return the error so it's propagated return fmt.Errorf("failed to load Harbor config: %w", err) } @@ -35,8 +43,7 @@ Case-insensitive field lookup, but uses the canonical (Go) field name internally // 3. Reflection-based delete (zero out) actualSegments := []string{} - if err := deleteValueInConfig(config, itemPath, &actualSegments); err != nil { - // Return the error for propagation + if err := deleteValueInConfig(config, itemPath, &actualSegments, credentialName); err != nil { return fmt.Errorf("failed to delete value in config: %w", err) } @@ -49,40 +56,61 @@ Case-insensitive field lookup, but uses the canonical (Go) field name internally canonicalPath := strings.Join(actualSegments, ".") logrus.Infof("Successfully cleared %s", canonicalPath) - // If everything succeeds, return nil return nil }, } + // Add --name / -n to let the user pick a specific credential + cmd.Flags().StringVarP( + &credentialName, + "name", + "n", + "", + "Name of the credential to delete fields from (default: the current credential)", + ) + return cmd } // deleteValueInConfig checks whether the user is deleting something -// under "credentials" (i.e., the current credential) or a top-level field. -func deleteValueInConfig(config *utils.HarborConfig, path []string, actualSegments *[]string) error { +// under "credentials" (i.e., *a* credential) or a top-level field. +// +// If the user says "credentials.*" AND provides --name, we'll look +// up that specific credential by name. Otherwise, we use CurrentCredentialName. +func deleteValueInConfig( + config *utils.HarborConfig, + path []string, + actualSegments *[]string, + credentialName string, +) error { if len(path) == 0 { return fmt.Errorf("no config item specified") } - // If the first segment is "credentials", then we pivot to the current credential. + // If the first segment is "credentials", pivot to the chosen credential. if strings.EqualFold(path[0], "credentials") { *actualSegments = append(*actualSegments, "Credentials") - // find the current credential - currentCredName := config.CurrentCredentialName - var currentCred *utils.Credential + // Figure out which credential name to use + credName := config.CurrentCredentialName + if credentialName != "" { + credName = credentialName + } + + // Find the matching credential + var targetCred *utils.Credential for i := range config.Credentials { - if strings.EqualFold(config.Credentials[i].Name, currentCredName) { - currentCred = &config.Credentials[i] + if strings.EqualFold(config.Credentials[i].Name, credName) { + targetCred = &config.Credentials[i] break } } - if currentCred == nil { - return fmt.Errorf("no matching credential found for '%s'", currentCredName) + if targetCred == nil { + return fmt.Errorf("no matching credential found for '%s'", credName) } - // Remove "credentials" from the path, and delete (zero) the value in that credential - return deleteNestedValue(currentCred, path[1:], actualSegments) + // Remove "credentials" from path, delete the value in that credential + return deleteNestedValue(targetCred, path[1:], actualSegments) } // Otherwise, we delete a field in the main HarborConfig struct @@ -140,10 +168,10 @@ func deleteNestedValue(obj interface{}, path []string, actualSegments *[]string) // If this is the last segment, set the field to zero value if !fieldValue.CanSet() { - return fmt.Errorf("cannot delete (set zero value) for field '%s'", field.Name) + return fmt.Errorf("cannot set field '%s' to zero value", field.Name) } - // The "zero" value for that field can be obtained with reflect.Zero(). + // The zero value for that field can be obtained with reflect.Zero(). zeroVal := reflect.Zero(fieldValue.Type()) fieldValue.Set(zeroVal) } diff --git a/cmd/harbor/root/config/get.go b/cmd/harbor/root/config/get.go index 4f9137f3..60d0e6e9 100644 --- a/cmd/harbor/root/config/get.go +++ b/cmd/harbor/root/config/get.go @@ -14,32 +14,40 @@ import ( // GetConfigItemCommand creates the 'harbor config get' subcommand. func GetConfigItemCommand() *cobra.Command { + var credentialName string + cmd := &cobra.Command{ - Use: "get ", - Short: "Get a specific config item", - Example: ` harbor config get credentials.username`, - Long: `Get the value of a specific CLI config item`, - Args: cobra.ExactArgs(1), + Use: "get ", + Short: "Get a specific config item", + Example: ` + # Get the current credential's username + harbor config get credentials.username + + # Get a credential's username by specifying the credential name + harbor config get credentials.username --name harbor-cli@http://demo.goharbor.io +`, + Long: `Get the value of a specific CLI config item. +If you specify --name, that credential (rather than the "current" one) will be used.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { // 1. Load config config, err := utils.GetCurrentHarborConfig() if err != nil { - // Return an error rather than just logging. return fmt.Errorf("failed to get config: %w", err) } - // 2. Parse the user-supplied item path (e.g. "credentials.username") + // 2. Parse the user-supplied item path (e.g., "credentials.username") itemPath := strings.Split(args[0], ".") // 3. Get the value from the config (and track actual field segments for output) actualSegments := []string{} - result, err := getValueFromConfig(config, itemPath, &actualSegments) + result, err := getValueFromConfig(config, itemPath, &actualSegments, credentialName) if err != nil { - // Return the error so it propagates to the caller/test. return err } - // 4. Prepare the final output as a map so we can render easily in JSON/YAML. + // 4. Prepare the final output as a map for JSON/YAML rendering. canonicalPath := strings.Join(actualSegments, ".") output := map[string]interface{}{ canonicalPath: result, @@ -66,45 +74,59 @@ func GetConfigItemCommand() *cobra.Command { return fmt.Errorf("unsupported output format: %s", formatFlag) } - // If everything succeeds, return nil. return nil }, } + // Add a --name / -n flag to allow specifying a credential + cmd.Flags().StringVarP( + &credentialName, + "name", + "n", + "", + "Name of the credential to get fields from (default: the current credential)", + ) + return cmd } // getValueFromConfig decides if the user requested something under "credentials" -// and if so, filters down to the current credential; otherwise, it just -// searches in the top-level config object. -// -// We also accept a pointer to 'actualSegments', so that if the user typed -// "credentials.Username", we can store the correct name for each field. E.g. "Credentials" -> "Username". -func getValueFromConfig(config *utils.HarborConfig, path []string, actualSegments *[]string) (interface{}, error) { +// and if so, filters down to the *requested credential*, otherwise +// it just searches in the top-level config object. +func getValueFromConfig( + config *utils.HarborConfig, + path []string, + actualSegments *[]string, + credentialName string, +) (interface{}, error) { if len(path) == 0 { return nil, fmt.Errorf("no config item specified") } - // If the first segment is "credentials", we pivot to the "current credential" - // and append the actual field name "Credentials" to 'actualSegments'. + // If the first segment is "credentials", we pivot to a credential. if strings.EqualFold(path[0], "credentials") { *actualSegments = append(*actualSegments, "Credentials") - // Find the current credential - currentCredName := config.CurrentCredentialName - var currentCred *utils.Credential - for _, cred := range config.Credentials { - if strings.EqualFold(cred.Name, currentCredName) { - currentCred = &cred + // Determine which credential name to use + credName := config.CurrentCredentialName + if credentialName != "" { + credName = credentialName + } + + // Find the matching credential + var targetCred *utils.Credential + for i := range config.Credentials { + if strings.EqualFold(config.Credentials[i].Name, credName) { + targetCred = &config.Credentials[i] break } } - if currentCred == nil { - return nil, fmt.Errorf("no matching credential found for '%s'", currentCredName) + if targetCred == nil { + return nil, fmt.Errorf("no matching credential found for '%s'", credName) } // Remove "credentials" from the path, keep the rest - return getNestedValue(*currentCred, path[1:], actualSegments) + return getNestedValue(*targetCred, path[1:], actualSegments) } // Otherwise, search in the overall config struct @@ -119,6 +141,7 @@ func getNestedValue(obj interface{}, path []string, actualSegments *[]string) (i current := reflect.ValueOf(obj) for _, key := range path { + // If it's a pointer, dereference if current.Kind() == reflect.Ptr { current = current.Elem() } diff --git a/cmd/harbor/root/config/update.go b/cmd/harbor/root/config/update.go index 71a5a50b..68239f1a 100644 --- a/cmd/harbor/root/config/update.go +++ b/cmd/harbor/root/config/update.go @@ -14,12 +14,21 @@ import ( // SetConfigItemCommand creates the 'harbor config set' subcommand, // allowing you to do: harbor config set . func SetConfigItemCommand() *cobra.Command { + var credentialName string + cmd := &cobra.Command{ - Use: "set ", - Short: "Set a specific config item", - Example: " harbor config set credentials.password myNewSecret", + Use: "set ", + Short: "Set a specific config item", + Example: ` + # Set the current credential's password + harbor config set credentials.password myNewSecret + + # Set a credential's password by specifying the credential name + harbor config set credentials.password myNewSecret --name harbor-cli@http://demo.goharbor.io +`, Long: `Set the value of a specific CLI config item. -Case-insensitive field lookup, but uses the canonical (Go) field name internally.`, +Case-insensitive field lookup, but uses the canonical (Go) field name internally. +If you specify --name, that credential (rather than the "current" one) will be updated.`, Args: cobra.ExactArgs(2), // Switch from Run to RunE so we can propagate errors @@ -27,7 +36,6 @@ Case-insensitive field lookup, but uses the canonical (Go) field name internally // 1. Load the current config config, err := utils.GetCurrentHarborConfig() if err != nil { - // Return the error (with context) instead of just logging return fmt.Errorf("failed to load Harbor config: %w", err) } @@ -37,7 +45,7 @@ Case-insensitive field lookup, but uses the canonical (Go) field name internally // 3. Reflection-based set actualSegments := []string{} - if err := setValueInConfig(config, itemPath, newValue, &actualSegments); err != nil { + if err := setValueInConfig(config, itemPath, newValue, &actualSegments, credentialName); err != nil { return fmt.Errorf("failed to set value in config: %w", err) } @@ -50,40 +58,63 @@ Case-insensitive field lookup, but uses the canonical (Go) field name internally canonicalPath := strings.Join(actualSegments, ".") logrus.Infof("Successfully updated %s to '%s'", canonicalPath, newValue) - // If everything is fine, return nil return nil }, } + // Add a --name / -n flag to allow specifying a credential + cmd.Flags().StringVarP( + &credentialName, + "name", + "n", + "", + "Name of the credential to set fields on (default: the current credential)", + ) + return cmd } // setValueInConfig checks whether the user is updating something -// under "credentials" (i.e., the current credential) or a top-level field. -func setValueInConfig(config *utils.HarborConfig, path []string, newValue string, actualSegments *[]string) error { +// under "credentials" (i.e., a credential) or a top-level field. +// +// If path[0] == "credentials", we decide which credential to modify: +// - If credentialName is non-empty, use that +// - Otherwise, fallback to config.CurrentCredentialName +func setValueInConfig( + config *utils.HarborConfig, + path []string, + newValue string, + actualSegments *[]string, + credentialName string, +) error { if len(path) == 0 { return fmt.Errorf("no config item specified") } - // If the first segment is "credentials", then we pivot to the current credential. + // If the first segment is "credentials", then we pivot to a specific credential. if strings.EqualFold(path[0], "credentials") { *actualSegments = append(*actualSegments, "Credentials") - // find the current credential - currentCredName := config.CurrentCredentialName - var currentCred *utils.Credential + // Determine which credential name to use + credName := config.CurrentCredentialName + if credentialName != "" { + credName = credentialName + } + + // find the matching credential + var matchingCred *utils.Credential for i := range config.Credentials { - if strings.EqualFold(config.Credentials[i].Name, currentCredName) { - currentCred = &config.Credentials[i] + if strings.EqualFold(config.Credentials[i].Name, credName) { + matchingCred = &config.Credentials[i] break } } - if currentCred == nil { - return fmt.Errorf("no matching credential found for '%s'", currentCredName) + if matchingCred == nil { + return fmt.Errorf("no matching credential found for '%s'", credName) } // Remove "credentials" from the path, and set the value in that credential - return setNestedValue(currentCred, path[1:], newValue, actualSegments) + return setNestedValue(matchingCred, path[1:], newValue, actualSegments) } // Otherwise, we set a field in the main HarborConfig struct @@ -176,6 +207,7 @@ func setNestedValue(obj interface{}, path []string, newValue string, actualSegme fieldValue.SetInt(intVal) // If you need to handle other types (e.g. float, slice), add them here. + default: return fmt.Errorf( "unsupported field type '%s' for field '%s'", diff --git a/pkg/utils/config.go b/pkg/utils/config.go index fbeff551..ba1b3dfe 100644 --- a/pkg/utils/config.go +++ b/pkg/utils/config.go @@ -329,8 +329,15 @@ func CreateConfigFile(configPath string) error { v.SetConfigType("yaml") defaultConfig := HarborConfig{ - CurrentCredentialName: "", - Credentials: []Credential{}, + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, } v.Set("current-credential-name", defaultConfig.CurrentCredentialName) diff --git a/test/e2e/config_cmd_test.go b/test/e2e/config_cmd_test.go index 38a3838c..3f67421f 100644 --- a/test/e2e/config_cmd_test.go +++ b/test/e2e/config_cmd_test.go @@ -82,6 +82,58 @@ func Test_ConfigGetCmd_Failure(t *testing.T) { assert.Error(t, err, "Expected an error when getting a non-existent config item") } +func Test_ConfigGetCmd_CredentialName_Success(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + testConfig := &utils.HarborConfig{ + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []utils.Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, + } + err := utils.UpdateConfigFile(testConfig) + if err != nil { + t.Fatal(err) + } + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "get", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"}) + err = rootCmd.Execute() + assert.NoError(t, err) +} + +func Test_ConfigGetCmd_CredentialName_Failure(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + testConfig := &utils.HarborConfig{ + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []utils.Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, + } + err := utils.UpdateConfigFile(testConfig) + if err != nil { + t.Fatal(err) + } + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "get", "credentials.serveraddress", "--name", "harbor-cli@http://goharbor.io"}) + err = rootCmd.Execute() + assert.Error(t, err, "Expected an error when getting a non-existent credential name") +} + func Test_ConfigSetCmd_Success(t *testing.T) { tempDir := t.TempDir() data := Initialize(t, tempDir) @@ -108,6 +160,58 @@ func Test_ConfigSetCmd_Success(t *testing.T) { assert.NoError(t, err) } +func Test_ConfigSetCmd_CredentialName_Success(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + testConfig := &utils.HarborConfig{ + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []utils.Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, + } + err := utils.UpdateConfigFile(testConfig) + if err != nil { + t.Fatal(err) + } + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "set", "credentials.serveraddress", "http://demo.goharbor.io", "--name", "harbor-cli@http://demo.goharbor.io"}) + err = rootCmd.Execute() + assert.NoError(t, err) +} + +func Test_ConfigSetCmd_CredentialName_Failure(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + testConfig := &utils.HarborConfig{ + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []utils.Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, + } + err := utils.UpdateConfigFile(testConfig) + if err != nil { + t.Fatal(err) + } + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "set", "credentials.serveraddress", "http://demo.goharbor.io", "--name", "harbor-cli@http://goharbor.io"}) + err = rootCmd.Execute() + assert.Error(t, err, "Expected an error when setting a non-existent credential name") +} + func Test_ConfigSetCmd_Failure(t *testing.T) { tempDir := t.TempDir() data := Initialize(t, tempDir) @@ -190,3 +294,60 @@ func Test_ConfigDeleteCmd_Failure(t *testing.T) { err = rootCmd.Execute() assert.Error(t, err, "Expected an error when deleting a non-existent config item") } + +func Test_ConfigDeleteCmd_CredentialName_Success(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + testConfig := &utils.HarborConfig{ + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []utils.Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, + } + err := utils.UpdateConfigFile(testConfig) + if err != nil { + t.Fatal(err) + } + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "delete", "credentials.serveraddress", "--name", "harbor-cli@http://demo.goharbor.io"}) + err = rootCmd.Execute() + assert.NoError(t, err) + config, err := utils.GetCurrentHarborConfig() + if err != nil { + t.Fatal(err) + } + assert.Empty(t, config.Credentials[0].ServerAddress) +} + +func Test_ConfigDeleteCmd_CredentialName_Failure(t *testing.T) { + tempDir := t.TempDir() + data := Initialize(t, tempDir) + defer ConfigCleanup(t, data) + SetMockKeyring(t) + testConfig := &utils.HarborConfig{ + CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + Credentials: []utils.Credential{ + { + Name: "harbor-cli@http://demo.goharbor.io", + ServerAddress: "http://demo.goharbor.io", + Username: "harbor-cli", + Password: "Harbor12345", + }, + }, + } + err := utils.UpdateConfigFile(testConfig) + if err != nil { + t.Fatal(err) + } + rootCmd := root.RootCmd() + rootCmd.SetArgs([]string{"config", "delete", "credentials.serveraddress", "--name", "harbor-cli@http://goharbor.io"}) + err = rootCmd.Execute() + assert.Error(t, err, "Expected an error when deleting a non-existent credential name") +} From 2cf64a26bce1d33f480bcce0e9fe06c4780e49be Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Sun, 22 Dec 2024 14:09:12 +0100 Subject: [PATCH 33/41] Did sign off rebase and mod tidy Signed-off-by: Patrick Eschenbach --- go.mod | 11 ++--------- go.sum | 10 +++------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 14840ae3..eb6c8ec3 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 + github.com/zalando/go-keyring v0.2.6 golang.org/x/term v0.6.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -22,11 +23,8 @@ require ( github.com/catppuccin/go v0.2.0 // indirect github.com/charmbracelet/x/ansi v0.2.3 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240725160154-f9f6568126ec // indirect - github.com/charmbracelet/x/input v0.1.3 // indirect - github.com/charmbracelet/x/term v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.1.2 // indirect - github.com/danieljoos/wincred v1.2.2 // indirect github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect @@ -53,11 +51,6 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect -<<<<<<< HEAD - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/zalando/go-keyring v0.2.6 // indirect -======= ->>>>>>> cb5437e (build(deps): bump github.com/charmbracelet/bubbles from 0.18.0 to 0.20.0 (#189)) go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/sync v0.8.0 // indirect diff --git a/go.sum b/go.sum index 5abe29b6..09b3f689 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/goharbor/go-client v0.210.0 h1:QwgLcWNSC3MFhBe7lq3BxDPtKQiD3k6hf6Lt26 github.com/goharbor/go-client v0.210.0/go.mod h1:XMWHucuHU9VTRx6U6wYwbRuyCVhE6ffJGRjaeo0nvwo= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -148,6 +150,7 @@ github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -158,13 +161,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -<<<<<<< HEAD -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= -======= ->>>>>>> cb5437e (build(deps): bump github.com/charmbracelet/bubbles from 0.18.0 to 0.20.0 (#189)) go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4= go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= @@ -184,8 +182,6 @@ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= From 0210af80648e6c01493183ff63243665b2ce2b12 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Wed, 4 Dec 2024 22:40:15 +0100 Subject: [PATCH 34/41] Explicitely set mock keyring in sub tests Signed-off-by: Patrick Eschenbach --- test/e2e/login_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/e2e/login_test.go b/test/e2e/login_test.go index 335d9934..7de4b427 100644 --- a/test/e2e/login_test.go +++ b/test/e2e/login_test.go @@ -31,6 +31,8 @@ func Test_Login_Success(t *testing.T) { assert.NoError(t, cmd.Flags().Set("username", "harbor-cli")) assert.NoError(t, cmd.Flags().Set("password", "Harbor12345")) + SetMockKeyring(t) + defer SetMockKeyring(t) err := cmd.Execute() assert.NoError(t, err, "Expected no error for server: %s", serverAddress) }) From 660695d5217c900e0b70c3fb88f5f566391edbcf Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Wed, 4 Dec 2024 22:46:16 +0100 Subject: [PATCH 35/41] Explicitely set mock keyring in sub tests Signed-off-by: Patrick Eschenbach --- test/e2e/login_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/e2e/login_test.go b/test/e2e/login_test.go index 7de4b427..335d9934 100644 --- a/test/e2e/login_test.go +++ b/test/e2e/login_test.go @@ -31,8 +31,6 @@ func Test_Login_Success(t *testing.T) { assert.NoError(t, cmd.Flags().Set("username", "harbor-cli")) assert.NoError(t, cmd.Flags().Set("password", "Harbor12345")) - SetMockKeyring(t) - defer SetMockKeyring(t) err := cmd.Execute() assert.NoError(t, err, "Expected no error for server: %s", serverAddress) }) From 297cadf0350fe2bb69da0a92521e1518b80816a3 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Sat, 21 Dec 2024 20:16:21 +0100 Subject: [PATCH 36/41] Added config sub command; added functions for set, get, list and clear config items; added a function to update config on disk Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/config/update.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/harbor/root/config/update.go b/cmd/harbor/root/config/update.go index 68239f1a..96131a14 100644 --- a/cmd/harbor/root/config/update.go +++ b/cmd/harbor/root/config/update.go @@ -207,7 +207,6 @@ func setNestedValue(obj interface{}, path []string, newValue string, actualSegme fieldValue.SetInt(intVal) // If you need to handle other types (e.g. float, slice), add them here. - default: return fmt.Errorf( "unsupported field type '%s' for field '%s'", From a50950f61323f88131184428713b2b82548ec6e8 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Sun, 22 Dec 2024 14:00:30 +0100 Subject: [PATCH 37/41] Added --name flag for credential selection; Added subcommand tests Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/config/update.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/harbor/root/config/update.go b/cmd/harbor/root/config/update.go index 96131a14..68239f1a 100644 --- a/cmd/harbor/root/config/update.go +++ b/cmd/harbor/root/config/update.go @@ -207,6 +207,7 @@ func setNestedValue(obj interface{}, path []string, newValue string, actualSegme fieldValue.SetInt(intVal) // If you need to handle other types (e.g. float, slice), add them here. + default: return fmt.Errorf( "unsupported field type '%s' for field '%s'", From 748893c1116b021f08f935d217e43d6b5aa7ee27 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Sun, 22 Dec 2024 15:23:11 +0100 Subject: [PATCH 38/41] Updated cli auto docs Signed-off-by: Patrick Eschenbach --- doc/cli-docs/harbor-config-delete.md | 51 ++++++++++++++++++++++++++++ doc/cli-docs/harbor-config-get.md | 50 +++++++++++++++++++++++++++ doc/cli-docs/harbor-config-list.md | 42 +++++++++++++++++++++++ doc/cli-docs/harbor-config-set.md | 51 ++++++++++++++++++++++++++++ doc/cli-docs/harbor-label-create.md | 46 +++++++++++++++++++++++++ doc/cli-docs/harbor-label-delete.md | 39 +++++++++++++++++++++ doc/cli-docs/harbor-label-list.md | 38 +++++++++++++++++++++ doc/cli-docs/harbor-label-update.md | 42 +++++++++++++++++++++++ doc/cli-docs/harbor-label.md | 32 +++++++++++++++++ 9 files changed, 391 insertions(+) create mode 100644 doc/cli-docs/harbor-config-delete.md create mode 100644 doc/cli-docs/harbor-config-get.md create mode 100644 doc/cli-docs/harbor-config-list.md create mode 100644 doc/cli-docs/harbor-config-set.md create mode 100644 doc/cli-docs/harbor-label-create.md create mode 100644 doc/cli-docs/harbor-label-delete.md create mode 100644 doc/cli-docs/harbor-label-list.md create mode 100644 doc/cli-docs/harbor-label-update.md create mode 100644 doc/cli-docs/harbor-label.md diff --git a/doc/cli-docs/harbor-config-delete.md b/doc/cli-docs/harbor-config-delete.md new file mode 100644 index 00000000..b9040b6b --- /dev/null +++ b/doc/cli-docs/harbor-config-delete.md @@ -0,0 +1,51 @@ +--- +title: harbor config delete +weight: 85 +--- +## harbor config delete + +### Description + +##### Delete (clear) a specific config item + +### Synopsis + +Clear the value of a specific CLI config item by setting it to its zero value. +Case-insensitive field lookup, but uses the canonical (Go) field name internally. +If you specify --name, that credential (rather than the "current" one) will be used. + +```sh +harbor config delete [flags] +``` + +### Examples + +```sh + + # Clear the current credential's password + harbor config delete credentials.password + + # Clear a specific credential's password using --name + harbor config delete credentials.password --name harbor-cli@http://demo.goharbor.io + +``` + +### Options + +```sh + -h, --help help for delete + -n, --name string Name of the credential to delete fields from (default: the current credential) +``` + +### 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 config](harbor-config.md) - Manage the config of the Harbor Cli + diff --git a/doc/cli-docs/harbor-config-get.md b/doc/cli-docs/harbor-config-get.md new file mode 100644 index 00000000..b9231f9b --- /dev/null +++ b/doc/cli-docs/harbor-config-get.md @@ -0,0 +1,50 @@ +--- +title: harbor config get +weight: 30 +--- +## harbor config get + +### Description + +##### Get a specific config item + +### Synopsis + +Get the value of a specific CLI config item. +If you specify --name, that credential (rather than the "current" one) will be used. + +```sh +harbor config get [flags] +``` + +### Examples + +```sh + + # Get the current credential's username + harbor config get credentials.username + + # Get a credential's username by specifying the credential name + harbor config get credentials.username --name harbor-cli@http://demo.goharbor.io + +``` + +### Options + +```sh + -h, --help help for get + -n, --name string Name of the credential to get fields from (default: the current credential) +``` + +### 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 config](harbor-config.md) - Manage the config of the Harbor Cli + diff --git a/doc/cli-docs/harbor-config-list.md b/doc/cli-docs/harbor-config-list.md new file mode 100644 index 00000000..c402e170 --- /dev/null +++ b/doc/cli-docs/harbor-config-list.md @@ -0,0 +1,42 @@ +--- +title: harbor config list +weight: 80 +--- +## harbor config list + +### Description + +##### List config items + +### Synopsis + +Get information of all CLI config items + +```sh +harbor config list [flags] +``` + +### Examples + +```sh + harbor config list +``` + +### Options + +```sh + -h, --help help for list +``` + +### 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 config](harbor-config.md) - Manage the config of the Harbor Cli + diff --git a/doc/cli-docs/harbor-config-set.md b/doc/cli-docs/harbor-config-set.md new file mode 100644 index 00000000..feb6b248 --- /dev/null +++ b/doc/cli-docs/harbor-config-set.md @@ -0,0 +1,51 @@ +--- +title: harbor config set +weight: 45 +--- +## harbor config set + +### Description + +##### Set a specific config item + +### Synopsis + +Set the value of a specific CLI config item. +Case-insensitive field lookup, but uses the canonical (Go) field name internally. +If you specify --name, that credential (rather than the "current" one) will be updated. + +```sh +harbor config set [flags] +``` + +### Examples + +```sh + + # Set the current credential's password + harbor config set credentials.password myNewSecret + + # Set a credential's password by specifying the credential name + harbor config set credentials.password myNewSecret --name harbor-cli@http://demo.goharbor.io + +``` + +### Options + +```sh + -h, --help help for set + -n, --name string Name of the credential to set fields on (default: the current credential) +``` + +### 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 config](harbor-config.md) - Manage the config of the Harbor Cli + diff --git a/doc/cli-docs/harbor-label-create.md b/doc/cli-docs/harbor-label-create.md new file mode 100644 index 00000000..d1ceb283 --- /dev/null +++ b/doc/cli-docs/harbor-label-create.md @@ -0,0 +1,46 @@ +--- +title: harbor label create +weight: 80 +--- +## harbor label create + +### Description + +##### create label + +### Synopsis + +create label in harbor + +```sh +harbor label create [flags] +``` + +### Examples + +```sh +harbor label create +``` + +### Options + +```sh + --color string Color of the label.color is in hex value (default "#FFFFFF") + -d, --description string Description of the label + -h, --help help for create + -n, --name string Name of the label + -s, --scope string Scope of the label. eg- g(global), p(specific project) (default "g") +``` + +### 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 label](harbor-label.md) - Manage labels in Harbor + diff --git a/doc/cli-docs/harbor-label-delete.md b/doc/cli-docs/harbor-label-delete.md new file mode 100644 index 00000000..9d9ced98 --- /dev/null +++ b/doc/cli-docs/harbor-label-delete.md @@ -0,0 +1,39 @@ +--- +title: harbor label delete +weight: 0 +--- +## harbor label delete + +### Description + +##### delete label + +```sh +harbor label delete [flags] +``` + +### Examples + +```sh +harbor label delete [labelname] +``` + +### Options + +```sh + -h, --help help for delete + -s, --scope string default(global).'p' for project labels.Query scope of the label (default "g") +``` + +### 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 label](harbor-label.md) - Manage labels in Harbor + diff --git a/doc/cli-docs/harbor-label-list.md b/doc/cli-docs/harbor-label-list.md new file mode 100644 index 00000000..99b09a46 --- /dev/null +++ b/doc/cli-docs/harbor-label-list.md @@ -0,0 +1,38 @@ +--- +title: harbor label list +weight: 15 +--- +## harbor label list + +### Description + +##### list labels + +```sh +harbor label list [flags] +``` + +### Options + +```sh + -h, --help help for list + --page int Page number (default 1) + --page-size int Size of per page (default 20) + -i, --projectid int project ID when query project labels (default 1) + -q, --query string Query string to query resources + -s, --scope string default(global).'p' for project labels.Query scope of the label (default "g") + --sort string Sort the label list in ascending or descending order +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor label](harbor-label.md) - Manage labels in Harbor + diff --git a/doc/cli-docs/harbor-label-update.md b/doc/cli-docs/harbor-label-update.md new file mode 100644 index 00000000..e2072cfe --- /dev/null +++ b/doc/cli-docs/harbor-label-update.md @@ -0,0 +1,42 @@ +--- +title: harbor label update +weight: 70 +--- +## harbor label update + +### Description + +##### update label + +```sh +harbor label update [flags] +``` + +### Examples + +```sh +harbor label update [labelname] +``` + +### Options + +```sh + --color string Color of the label.color is in hex value + -d, --description string Description of the label + -h, --help help for update + -n, --name string Name of the label + -s, --scope string Scope of the label. eg- g(global), p(specific project) (default "g") +``` + +### 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 label](harbor-label.md) - Manage labels in Harbor + diff --git a/doc/cli-docs/harbor-label.md b/doc/cli-docs/harbor-label.md new file mode 100644 index 00000000..5171e162 --- /dev/null +++ b/doc/cli-docs/harbor-label.md @@ -0,0 +1,32 @@ +--- +title: harbor label +weight: 65 +--- +## harbor label + +### Description + +##### Manage labels in Harbor + +### Options + +```sh + -h, --help help for label +``` + +### Options inherited from parent commands + +```sh + -c, --config string config file (default is $HOME/.config/harbor-cli/config.yaml) + -o, --output-format string Output format. One of: json|yaml + -v, --verbose verbose output +``` + +### SEE ALSO + +* [harbor](harbor.md) - Official Harbor CLI +* [harbor label create](harbor-label-create.md) - create label +* [harbor label delete](harbor-label-delete.md) - delete label +* [harbor label list](harbor-label-list.md) - list labels +* [harbor label update](harbor-label-update.md) - update label + From 87e6492c10c3c3db3585ce58c35e8f11b102673c Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Sun, 22 Dec 2024 16:03:05 +0100 Subject: [PATCH 39/41] Fix bug to add new credentials in case others already exist Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/login.go | 54 +++++++++++++++++++++++++++++++++------- pkg/utils/config.go | 4 +-- pkg/utils/encryption.go | 16 ++++++++++-- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/cmd/harbor/root/login.go b/cmd/harbor/root/login.go index d97089f5..bea187be 100644 --- a/cmd/harbor/root/login.go +++ b/cmd/harbor/root/login.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strings" "github.com/goharbor/go-client/pkg/harbor" "github.com/goharbor/go-client/pkg/sdk/v2.0/client/user" @@ -68,22 +69,59 @@ func LoginCommand() *cobra.Command { if loginView.Server != "" && loginView.Username != "" && loginView.Password != "" { err = runLogin(loginView) - } else if currentCredentialName != "" { + // check whether the user entered a new credential than already in config + } else if currentCredentialName != "" && loginView.Name != "" && loginView.Name != currentCredentialName { var resolvedLoginView login.LoginView creds = config.Credentials for _, cred := range creds { - resolvedLoginView = login.LoginView{ - Server: cred.ServerAddress, - Username: cred.Username, - Password: cred.Password, - Name: cred.Name, + if cred.Name == loginView.Name { + resolvedLoginView = login.LoginView{ + Server: cred.ServerAddress, + Username: cred.Username, + Password: cred.Password, + Name: cred.Name, + } + } + } + if resolvedLoginView.Server != "" && resolvedLoginView.Username != "" && resolvedLoginView.Password != "" { + err = runLogin(resolvedLoginView) + } else { + err = createLoginView(&loginView) + } + } else if currentCredentialName != "" { + var resolvedLoginView login.LoginView + creds := config.Credentials + key, err := utils.GetEncryptionKey() + if err != nil { + return fmt.Errorf("failed to get encryption key: %w", err) + } + var targetCred *utils.Credential + for _, cred := range creds { + if strings.EqualFold(cred.Name, currentCredentialName) { + targetCred = &cred + break } } + if targetCred == nil { + return fmt.Errorf("no matching credential found for '%s'", currentCredentialName) + } + decryptedPassword, err := utils.Decrypt(key, string(targetCred.Password)) + if err != nil { + return fmt.Errorf("failed to decrypt password: %w", err) + } + resolvedLoginView = login.LoginView{ + Server: targetCred.ServerAddress, + Username: targetCred.Username, + Password: decryptedPassword, + Name: targetCred.Name, + } err = runLogin(resolvedLoginView) + if err != nil { + return fmt.Errorf("failed to run login: %w", err) + } } else { err = createLoginView(&loginView) } - if err != nil { return err } @@ -123,13 +161,11 @@ func runLogin(opts login.LoginView) error { Password: opts.Password, } client := utils.GetClientByConfig(clientConfig) - ctx := context.Background() _, err := client.User.GetCurrentUserInfo(ctx, &user.GetCurrentUserInfoParams{}) if err != nil { return fmt.Errorf("login failed, please check your credentials: %s", err) } - if err := utils.GenerateEncryptionKey(); err != nil { fmt.Println("Encryption key already exists or could not be created:", err) } diff --git a/pkg/utils/config.go b/pkg/utils/config.go index ba1b3dfe..a7ee9f99 100644 --- a/pkg/utils/config.go +++ b/pkg/utils/config.go @@ -329,11 +329,11 @@ func CreateConfigFile(configPath string) error { v.SetConfigType("yaml") defaultConfig := HarborConfig{ - CurrentCredentialName: "harbor-cli@http://demo.goharbor.io", + CurrentCredentialName: "harbor-cli@@emo-goharbor-io", Credentials: []Credential{ { Name: "harbor-cli@http://demo.goharbor.io", - ServerAddress: "http://demo.goharbor.io", + ServerAddress: "https://demo.goharbor.io", Username: "harbor-cli", Password: "Harbor12345", }, diff --git a/pkg/utils/encryption.go b/pkg/utils/encryption.go index a57476b6..c4b8a87d 100644 --- a/pkg/utils/encryption.go +++ b/pkg/utils/encryption.go @@ -41,6 +41,11 @@ const KeyringService = "harbor-cli" const KeyringUser = "harbor-cli-encryption-key" func GenerateEncryptionKey() error { + existingKey, err := keyringProvider.Get(KeyringService, KeyringUser) + if err == nil && existingKey != "" { + return nil + } + key := make([]byte, 32) // AES-256 key if _, err := rand.Read(key); err != nil { return fmt.Errorf("failed to generate encryption key: %w", err) @@ -50,8 +55,15 @@ func GenerateEncryptionKey() error { func GetEncryptionKey() ([]byte, error) { keyBase64, err := keyringProvider.Get(KeyringService, KeyringUser) - if err != nil { - return nil, fmt.Errorf("failed to retrieve encryption key: %w", err) + if err != nil || keyBase64 == "" { + // Attempt to generate a new key if not found + if genErr := GenerateEncryptionKey(); genErr != nil { + return nil, fmt.Errorf("failed to retrieve or generate encryption key: %w", err) + } + keyBase64, err = keyringProvider.Get(KeyringService, KeyringUser) + if err != nil { + return nil, fmt.Errorf("failed to retrieve encryption key after generation: %w", err) + } } return base64.StdEncoding.DecodeString(keyBase64) } From 49f1d5bb986ca242199d318231f651378eb1fb95 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Sun, 22 Dec 2024 16:11:22 +0100 Subject: [PATCH 40/41] Fix bug to add new credentials in case others already exist; fix bug in config with default config Signed-off-by: Patrick Eschenbach --- pkg/utils/config.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/utils/config.go b/pkg/utils/config.go index a7ee9f99..2a5862fd 100644 --- a/pkg/utils/config.go +++ b/pkg/utils/config.go @@ -328,14 +328,24 @@ func CreateConfigFile(configPath string) error { v := viper.New() v.SetConfigType("yaml") + key, err := GetEncryptionKey() + if err != nil { + fmt.Println("Error getting encryption key:", err) + return fmt.Errorf("failed to get encryption key: %s", err) + } + encryptedPassword, err := Encrypt(key, []byte("Harbor12345")) + if err != nil { + fmt.Println("Error encrypting password:", err) + return fmt.Errorf("failed to encrypt password: %s", err) + } defaultConfig := HarborConfig{ - CurrentCredentialName: "harbor-cli@@emo-goharbor-io", + CurrentCredentialName: "harbor-cli@demo-goharbor-io", Credentials: []Credential{ { - Name: "harbor-cli@http://demo.goharbor.io", + Name: "harbor-cli@demo-goharbor-io", ServerAddress: "https://demo.goharbor.io", Username: "harbor-cli", - Password: "Harbor12345", + Password: encryptedPassword, }, }, } From b9d37543fe9be16a39c9257aa5092c4c478adf90 Mon Sep 17 00:00:00 2001 From: Patrick Eschenbach Date: Mon, 23 Dec 2024 13:24:18 +0100 Subject: [PATCH 41/41] Fix for login command with not all flags; Added automatic context switch after login execution and credential update Signed-off-by: Patrick Eschenbach --- cmd/harbor/root/login.go | 2 +- pkg/utils/config.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/harbor/root/login.go b/cmd/harbor/root/login.go index bea187be..68f45139 100644 --- a/cmd/harbor/root/login.go +++ b/cmd/harbor/root/login.go @@ -88,7 +88,7 @@ func LoginCommand() *cobra.Command { } else { err = createLoginView(&loginView) } - } else if currentCredentialName != "" { + } else if currentCredentialName != "" && loginView.Name == "" && loginView.Server == "" && loginView.Username == "" && loginView.Password == "" { var resolvedLoginView login.LoginView creds := config.Credentials key, err := utils.GetEncryptionKey() diff --git a/pkg/utils/config.go b/pkg/utils/config.go index 2a5862fd..6549e160 100644 --- a/pkg/utils/config.go +++ b/pkg/utils/config.go @@ -485,6 +485,7 @@ func UpdateCredentialsInConfigFile(updatedCredential Credential, configPath stri for i, cred := range c.Credentials { if cred.Name == updatedCredential.Name { c.Credentials[i] = updatedCredential + c.CurrentCredentialName = updatedCredential.Name updated = true break } @@ -501,6 +502,7 @@ func UpdateCredentialsInConfigFile(updatedCredential Credential, configPath stri log.Fatalf("failed to write updated config file: %v", err) } - log.Infof("Updated credential '%s' in config file at %s", updatedCredential.Name, configPath) + log.Infof("Updated credential '%s' in config file at %s.", updatedCredential.Name, configPath) + log.Infof("Switched to context '%s'", updatedCredential.Name) return nil }