Skip to content

Commit

Permalink
Support oras login (oras-project#53)
Browse files Browse the repository at this point in the history
Supports
- `oras login` to log in to a remote registry.
- `oras logout` to log out from a remote registry.
  • Loading branch information
shizhMSFT authored Apr 11, 2019
1 parent c940067 commit 1829eb0
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 27 deletions.
6 changes: 5 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

141 changes: 141 additions & 0 deletions cmd/oras/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package main

import (
"bufio"
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"

auth "github.com/deislabs/oras/pkg/auth/docker"

"github.com/docker/docker/pkg/term"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

type loginOptions struct {
hostname string
fromStdin bool

debug bool
configs []string
username string
password string
}

func loginCmd() *cobra.Command {
var opts loginOptions
cmd := &cobra.Command{
Use: "login registry",
Short: "Log in to a remote registry",
Long: `Log in to a remote registry
Example - Login with username and password from command line:
oras login -u username -p password localhost:5000
Example - Login with username and password from stdin:
oras login -u username --password-stdin localhost:5000
Example - Login with identity token from command line:
oras login -p token localhost:5000
Example - Login with identity token from stdin:
oras login --password-stdin localhost:5000
Example - Login with username and password by prompt:
oras login localhost:5000
`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.hostname = args[0]
return runLogin(opts)
},
}

cmd.Flags().BoolVarP(&opts.debug, "debug", "d", false, "debug mode")
cmd.Flags().StringArrayVarP(&opts.configs, "config", "c", nil, "auth config path")
cmd.Flags().StringVarP(&opts.username, "username", "u", "", "registry username")
cmd.Flags().StringVarP(&opts.password, "password", "p", "", "registry password or identity token")
cmd.Flags().BoolVarP(&opts.fromStdin, "password-stdin", "", false, "read password or identity token from stdin")
return cmd
}

func runLogin(opts loginOptions) error {
if opts.debug {
logrus.SetLevel(logrus.DebugLevel)
}

// Prepare auth client
cli, err := auth.NewClient(opts.configs...)
if err != nil {
return err
}

// Prompt credential
if opts.fromStdin {
password, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return err
}
opts.password = strings.TrimSuffix(string(password), "\n")
opts.password = strings.TrimSuffix(opts.password, "\r")
} else if opts.password == "" {
if opts.username == "" {
username, err := readLine("Username: ", false)
if err != nil {
return err
}
opts.username = strings.TrimSpace(username)
}
if opts.username == "" {
if opts.password, err = readLine("Token: ", true); err != nil {
return err
} else if opts.password == "" {
return errors.New("token required")
}
} else {
if opts.password, err = readLine("Password: ", true); err != nil {
return err
} else if opts.password == "" {
return errors.New("password required")
}
}
} else {
fmt.Fprintln(os.Stderr, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
}

// Login
if err := cli.Login(context.Background(), opts.hostname, opts.username, opts.password); err != nil {
return err
}

fmt.Println("Login Succeeded")
return nil
}

func readLine(prompt string, slient bool) (string, error) {
fmt.Print(prompt)
if slient {
fd := os.Stdin.Fd()
state, err := term.SaveState(fd)
if err != nil {
return "", err
}
term.DisableEcho(fd, state)
defer term.RestoreTerminal(fd, state)
}

reader := bufio.NewReader(os.Stdin)
line, _, err := reader.ReadLine()
if err != nil {
return "", err
}
if slient {
fmt.Println()
}

return string(line), nil
}
52 changes: 52 additions & 0 deletions cmd/oras/logout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"context"

auth "github.com/deislabs/oras/pkg/auth/docker"

"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

type logoutOptions struct {
hostname string

debug bool
configs []string
}

func logoutCmd() *cobra.Command {
var opts logoutOptions
cmd := &cobra.Command{
Use: "logout registry",
Short: "Log out from a remote registry",
Long: `Log out from a remote registry
Example - Logout:
oras logout localhost:5000
`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.hostname = args[0]
return runLogout(opts)
},
}

cmd.Flags().BoolVarP(&opts.debug, "debug", "d", false, "debug mode")
cmd.Flags().StringArrayVarP(&opts.configs, "config", "c", nil, "auth config path")
return cmd
}

func runLogout(opts logoutOptions) error {
if opts.debug {
logrus.SetLevel(logrus.DebugLevel)
}

cli, err := auth.NewClient(opts.configs...)
if err != nil {
return err
}

return cli.Logout(context.Background(), opts.hostname)
}
2 changes: 1 addition & 1 deletion cmd/oras/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func main() {
Use: "oras [command]",
SilenceUsage: true,
}
cmd.AddCommand(pullCmd(), pushCmd())
cmd.AddCommand(pullCmd(), pushCmd(), loginCmd(), logoutCmd())
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type pullOptions struct {
verbose bool

debug bool
configs []string
username string
password string
}
Expand Down Expand Up @@ -56,6 +57,7 @@ Example - Pull all files, any media type:
cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", false, "verbose output")

cmd.Flags().BoolVarP(&opts.debug, "debug", "d", false, "debug mode")
cmd.Flags().StringArrayVarP(&opts.configs, "config", "c", nil, "auth config path")
cmd.Flags().StringVarP(&opts.username, "username", "u", "", "registry username")
cmd.Flags().StringVarP(&opts.password, "password", "p", "", "registry password")
return cmd
Expand All @@ -71,7 +73,7 @@ func runPull(opts pullOptions) error {
opts.allowedMediaTypes = []string{content.DefaultBlobMediaType}
}

resolver := newResolver(opts.username, opts.password)
resolver := newResolver(opts.username, opts.password, opts.configs...)
store := content.NewFileStore(opts.output)
store.DisableOverwrite = opts.keepOldFiles
store.AllowPathTraversalOnWrite = opts.pathTraversal
Expand Down
4 changes: 3 additions & 1 deletion cmd/oras/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type pushOptions struct {
manifestAnnotations string

debug bool
configs []string
username string
password string
}
Expand Down Expand Up @@ -60,6 +61,7 @@ Example - Push file "hi.txt" with the custom manifest config "config.json" of th
cmd.Flags().StringVarP(&opts.manifestConfigRef, "manifest-config", "", "", "manifest config file")
cmd.Flags().StringVarP(&opts.manifestAnnotations, "manifest-annotations", "", "", "manifest annotation file")
cmd.Flags().BoolVarP(&opts.debug, "debug", "d", false, "debug mode")
cmd.Flags().StringArrayVarP(&opts.configs, "config", "c", nil, "auth config path")
cmd.Flags().StringVarP(&opts.username, "username", "u", "", "registry username")
cmd.Flags().StringVarP(&opts.password, "password", "p", "", "registry password")
return cmd
Expand Down Expand Up @@ -128,7 +130,7 @@ func runPush(opts pushOptions) error {
}

// ready to push
resolver := newResolver(opts.username, opts.password)
resolver := newResolver(opts.username, opts.password, opts.configs...)
return oras.Push(context.Background(), resolver, opts.targetRef, store, files, pushOpts...)
}

Expand Down
43 changes: 20 additions & 23 deletions cmd/oras/resolver.go
Original file line number Diff line number Diff line change
@@ -1,35 +1,32 @@
package main

import (
"context"
"fmt"
"os"

auth "github.com/deislabs/oras/pkg/auth/docker"

"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
"github.com/docker/cli/cli/config"
"github.com/docker/docker/registry"
)

func newResolver(username, password string) remotes.Resolver {
cfg := config.LoadDefaultConfigFile(os.Stderr)
credential := func(hostName string) (string, string, error) {
if hostName == registry.DefaultV2Registry.Host {
hostName = registry.IndexServer
}
auth, err := cfg.GetAuthConfig(hostName)
if err != nil {
return "", "", err
}
if auth.IdentityToken != "" {
return "", auth.IdentityToken, nil
}
return auth.Username, auth.Password, nil
}
func newResolver(username, password string, configs ...string) remotes.Resolver {
if username != "" || password != "" {
credential = func(hostName string) (string, string, error) {
return username, password, nil
}
return docker.NewResolver(docker.ResolverOptions{
Credentials: func(hostName string) (string, string, error) {
return username, password, nil
},
})
}
cli, err := auth.NewClient(configs...)
if err != nil {
fmt.Fprintf(os.Stderr, "WARNING: Error loading auth file: %v\n", err)
}
resolver, err := cli.Resolver(context.Background())
if err != nil {
fmt.Fprintf(os.Stderr, "WARNING: Error loading resolver: %v\n", err)
resolver = docker.NewResolver(docker.ResolverOptions{})
}
return docker.NewResolver(docker.ResolverOptions{
Credentials: credential,
})
return resolver
}
17 changes: 17 additions & 0 deletions pkg/auth/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package auth

import (
"context"

"github.com/containerd/containerd/remotes"
)

// Client provides authentication operations for remotes.
type Client interface {
// Login logs in to a remote server identified by the hostname.
Login(ctx context.Context, hostname, username, secret string) error
// Logout logs out from a remote server identified by the hostname.
Logout(ctx context.Context, hostname string) error
// Resolver returns a new authenticated resolver.
Resolver(ctx context.Context) (remotes.Resolver, error)
}
Loading

0 comments on commit 1829eb0

Please sign in to comment.