Skip to content

Commit

Permalink
Add login command
Browse files Browse the repository at this point in the history
This commit adds the `login` command for the
user to log in to the server address passed.

This will enable users to login to multiple
registeries and specific which set of credentials
to use for other subcommands via cli or env var
else it uses the current credential set in the
config file.

This commit also adds the utils functions that
will be used by successive commits.

Signed-off-by: Akshat <akshat25iiit@gmail.com>
  • Loading branch information
akshatdalton committed Apr 30, 2023
1 parent 81b403e commit 49ee28f
Show file tree
Hide file tree
Showing 8 changed files with 688 additions and 0 deletions.
5 changes: 5 additions & 0 deletions cmd/constants/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package constants

const (
HarborCredentialName = "HARBORCREDENTIALNAME"
)
75 changes: 75 additions & 0 deletions cmd/login/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package login

import (
"context"
"fmt"

"github.com/akshatdalton/harbor-cli/cmd/utils"
"github.com/goharbor/go-client/pkg/harbor"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client/user"
"github.com/spf13/cobra"
)

type loginOptions struct {
name string
serverAddress string
username string
password string
}

// NewLoginCommand creates a new `harbor login` command
func NewLoginCommand() *cobra.Command {
var opts loginOptions

cmd := &cobra.Command{
Use: "login [SERVER]",
Short: "Log in to Harbor registry",
Long: "Authenticate with Harbor Registry.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.serverAddress = args[0]
return runLogin(opts)
},
}

flags := cmd.Flags()
flags.StringVarP(&opts.name, "name", "", "", "name for the set of credentials")

flags.StringVarP(&opts.username, "username", "u", "", "Username")
if err := cmd.MarkFlagRequired("username"); err != nil {
panic(err)
}
flags.StringVarP(&opts.password, "password", "p", "", "Password")
if err := cmd.MarkFlagRequired("password"); err != nil {
panic(err)
}

return cmd
}

func runLogin(opts loginOptions) error {
clientConfig := &harbor.ClientSetConfig{
URL: opts.serverAddress,
Username: opts.username,
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)
}

cred := utils.Credential{
Name: opts.name,
Username: opts.username,
Password: opts.password,
ServerAddress: opts.serverAddress,
}

if err = utils.StoreCredential(cred, true); err != nil {
return fmt.Errorf("Failed to store the credential: %s", err)
}
return nil
}
21 changes: 21 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package cmd

import (
"github.com/akshatdalton/harbor-cli/cmd/login"
"github.com/spf13/cobra"
)

func addCommands(cmd *cobra.Command) {
cmd.AddCommand(login.NewLoginCommand())
}

// CreateHarborCLI creates a new Harbor CLI
func CreateHarborCLI() *cobra.Command {
cmd := &cobra.Command{
Use: "harbor",
Short: "Official Harbor CLI",
}

addCommands(cmd)
return cmd
}
142 changes: 142 additions & 0 deletions cmd/utils/credential_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package utils

import (
"fmt"
"io/ioutil"
"log"
"net/url"
"os"
"path/filepath"

"github.com/adrg/xdg"
"github.com/akshatdalton/harbor-cli/cmd/constants"
"gopkg.in/yaml.v2"
)

var configFile = filepath.Join(xdg.Home, ".harbor", "config")

type Credential struct {
Name string `yaml:"name"`
Username string `yaml:"username"`
Password string `yaml:"password"`
ServerAddress string `yaml:"serveraddress"`
}

type CredentialStore struct {
CurrentCredentialName string `yaml:"current-credential-name"`
Credentials []Credential `yaml:"credentials"`
}

func checkAndUpdateCredentialName(credential *Credential) {
if credential.Name != "" {
return
}

parsedUrl, err := url.Parse(credential.ServerAddress)
if err != nil {
panic(err)
}

credential.Name = parsedUrl.Hostname() + "-" + credential.Username
log.Println("credential name not specified, storing the credential with the name as:", credential.Name)
return
}

func readCredentialStore() (CredentialStore, error) {
configInfo, err := ioutil.ReadFile(configFile)
if err != nil {
return CredentialStore{}, err
}

var credentialStore CredentialStore
if err := yaml.Unmarshal(configInfo, &credentialStore); err != nil {
return CredentialStore{}, err
}
return credentialStore, nil
}

func checkAndCreateConfigFile() {
if _, err := os.Stat(configFile); os.IsNotExist(err) {
// Create the parent directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(configFile), os.ModePerm); err != nil {
panic(err)
}

if _, err := os.Create(configFile); err != nil {
panic(err)
}
} else if err != nil {
panic(err)
}
}

func StoreCredential(credential Credential, setAsCurrentCredential bool) error {
checkAndUpdateCredentialName(&credential)
checkAndCreateConfigFile()
credentialStore, err := readCredentialStore()
if err != nil {
return fmt.Errorf("failed to read credential store: %s", err)
}

// Check and remove the credential with same username and serveraddress.
removeIndex := -1
for i, cred := range credentialStore.Credentials {
if cred.Username == credential.Username && cred.ServerAddress == credential.ServerAddress {
removeIndex = i
break
}
}

if removeIndex != -1 {
credentialStore.Credentials = append(credentialStore.Credentials[:removeIndex], credentialStore.Credentials[removeIndex+1:]...)
}

credentialStore.Credentials = append(credentialStore.Credentials, credential)
if setAsCurrentCredential {
credentialStore.CurrentCredentialName = credential.Name
}

bytes, err := yaml.Marshal(credentialStore)
if err != nil {
return err
}
if err = ioutil.WriteFile(configFile, bytes, 0600); err != nil {
return err
}
log.Println("Saving credentials to:", configFile)
return nil
}

// resolveCredential resolves the credential in the following priority order:
// 1. credentialName specified by the user via CLI argument
// 2. credentialName specified by the user via environment variable
// 3. current active credential
func resolveCredential(credentialName string) (Credential, error) {
credentialStore, err := readCredentialStore()
if err != nil {
panic(fmt.Sprintf("failed to read credential store: %s", err))
}

// If credentialName is not specified, check environment variable
if credentialName == "" {
credentialName = os.Getenv(constants.HarborCredentialName)
}

// If user has not specified the credential to use, use the current active credential
if credentialName == "" {
credentialName = credentialStore.CurrentCredentialName
}

if credentialName == "" {
return Credential{}, fmt.Errorf("current credential name not set, please login again")
}

// Look for the credential with the given name
for _, cred := range credentialStore.Credentials {
if cred.Name == credentialName {
return cred, nil
}
}

return Credential{}, fmt.Errorf("no credential found for the name: %s, please login again with the credential name: %s", credentialName, credentialName)
}
45 changes: 45 additions & 0 deletions cmd/utils/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package utils

import (
"encoding/json"
"fmt"

"github.com/goharbor/go-client/pkg/harbor"
v2client "github.com/goharbor/go-client/pkg/sdk/v2.0/client"
)

// Returns Harbor v2 client for given clientConfig
func GetClientByConfig(clientConfig *harbor.ClientSetConfig) *v2client.HarborAPI {
cs, err := harbor.NewClientSet(clientConfig)
if err != nil {
panic(err)
}
return cs.V2()
}

// Returns Harbor v2 client after resolving the credential name
func GetClientByCredentialName(credentialName string) *v2client.HarborAPI {
credential, err := resolveCredential(credentialName)
if err != nil {
panic(err)
}
clientConfig := &harbor.ClientSetConfig{
URL: credential.ServerAddress,
Username: credential.Username,
Password: credential.Password,
}
return GetClientByConfig(clientConfig)
}

func PrintPayloadInJSONFormat(payload any) {
if payload == nil {
return
}

jsonStr, err := json.MarshalIndent(payload, "", " ")
if err != nil {
panic(err)
}

fmt.Println(string(jsonStr))
}
41 changes: 41 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module github.com/akshatdalton/harbor-cli

go 1.20

require github.com/spf13/cobra v1.7.0

require (
github.com/google/go-cmp v0.5.9 // indirect
github.com/stretchr/testify v1.8.1 // indirect
)

require (
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/adrg/xdg v0.4.0
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/go-openapi/analysis v0.20.1 // indirect
github.com/go-openapi/errors v0.20.1 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/loads v0.21.0 // indirect
github.com/go-openapi/runtime v0.21.0 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/strfmt v0.21.0 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-openapi/validate v0.20.3 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/goharbor/go-client v0.26.2
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.mongodb.org/mongo-driver v1.7.3 // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
gopkg.in/yaml.v2 v2.4.0
)
Loading

0 comments on commit 49ee28f

Please sign in to comment.