Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move reusable options to a dedicated package #386

Merged
merged 18 commits into from
May 19, 2022
40 changes: 40 additions & 0 deletions cmd/oras/internal/option/applier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package option
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
qweeah marked this conversation as resolved.
Show resolved Hide resolved

import (
"reflect"

"github.com/spf13/pflag"
)

// FlagApplier applies flags to a command flag set.
type FlagApplier interface {
ApplyFlags(*pflag.FlagSet)
}

// ApplyFlags applies applicable fields of the passed-in option pointer to the
// target flag set.
// NOTE: The option argument need to be a pointer to the options, so its value
// is on heap and addressable.
qweeah marked this conversation as resolved.
Show resolved Hide resolved
func ApplyFlags(optsPtr interface{}, target *pflag.FlagSet) {
v := reflect.ValueOf(optsPtr).Elem()
for i := 0; i < v.NumField(); i++ {
iface := v.Field(i).Addr().Interface()
if a, ok := iface.(FlagApplier); ok {
a.ApplyFlags(target)
}
}
}
48 changes: 48 additions & 0 deletions cmd/oras/internal/option/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package option

import (
"context"

"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
"oras.land/oras/internal/trace"
)

// Common option struct.
type Common struct {
Debug bool
Verbose bool
}

// ApplyFlags applies flags to a command flag set.
func (common *Common) ApplyFlags(fs *pflag.FlagSet) {
fs.BoolVarP(&common.Debug, "debug", "d", false, "debug mode")
fs.BoolVarP(&common.Verbose, "verbose", "v", false, "verbose output")
}

// SetLoggerLevel sets up the logger based on common options.
func (common *Common) SetLoggerLevel() (context.Context, logrus.FieldLogger) {
var logLevel logrus.Level
if common.Debug {
logLevel = logrus.DebugLevel
} else if common.Verbose {
logLevel = logrus.InfoLevel
} else {
logLevel = logrus.WarnLevel
}
return trace.WithLoggerLevel(context.Background(), logLevel)
}
56 changes: 56 additions & 0 deletions cmd/oras/internal/option/credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package option

import (
"fmt"
"io"
"os"
"strings"

"github.com/spf13/pflag"
)

// Credential options struct.
type Credential struct {
Configs []string
Username string
PasswordFromStdin bool
Password string
}

// ApplyFlags applies flags to a command flag set.
func (cred *Credential) ApplyFlags(fs *pflag.FlagSet) {
fs.StringArrayVarP(&cred.Configs, "config", "c", nil, "auth config path")
fs.StringVarP(&cred.Username, "username", "u", "", "registry username")
fs.StringVarP(&cred.Password, "password", "p", "", "registry password or identity token")
fs.BoolVarP(&cred.PasswordFromStdin, "password-stdin", "", false, "read password or identity token from stdin")
}

// ReadPassword tries to read password with optional cmd prompt.
func (cred *Credential) ReadPassword() (err error) {
if cred.Password != "" {
fmt.Fprintln(os.Stderr, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
} else if cred.PasswordFromStdin {
// Prompt for credential
password, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
cred.Password = strings.TrimSuffix(string(password), "\n")
cred.Password = strings.TrimSuffix(cred.Password, "\r")
}
return nil
}
38 changes: 38 additions & 0 deletions cmd/oras/internal/option/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package option
qweeah marked this conversation as resolved.
Show resolved Hide resolved

import (
ctls "crypto/tls"
"crypto/x509"

"github.com/spf13/pflag"
"oras.land/oras/internal/http"
)

// TLS option struct.
type TLS struct {
CACertFilePath string
PlainHTTP bool
Insecure bool
}

// ApplyFlags applies flags to a command flag set.
func (tls *TLS) ApplyFlags(fs *pflag.FlagSet) {
fs.BoolVarP(&tls.Insecure, "insecure", "k", false, "allow connections to SSL registry without certs")
fs.StringVarP(&tls.CACertFilePath, "ca-file", "", "", "server certificate authority file for the remote registry")
fs.BoolVarP(&tls.PlainHTTP, "plain-http", "", false, "allow insecure connections to registry without SSL")
}

// Config assembles the tls config.
func (tls *TLS) Config() (config *ctls.Config, err error) {
config = &ctls.Config{}
var caPool *x509.CertPool
if tls.CACertFilePath == "" {
caPool = nil
} else if caPool, err = http.LoadCertPool(tls.CACertFilePath); err != nil {
return nil, err
}

config.RootCAs = caPool
config.InsecureSkipVerify = tls.Insecure
return
}
111 changes: 40 additions & 71 deletions cmd/oras/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,24 @@ package main

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

"github.com/moby/term"
qweeah marked this conversation as resolved.
Show resolved Hide resolved
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"oras.land/oras-go/v2/registry/remote"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/internal/credential"
"oras.land/oras/internal/http"
"oras.land/oras/internal/trace"
)

type loginOptions struct {
hostname string
fromStdin bool

debug bool
configs []string
caFilePath string
username string
password string
insecure bool
plainHTTP bool
verbose bool
option.Common
option.Credential
option.TLS
Hostname string
}

func loginCmd() *cobra.Command {
Expand Down Expand Up @@ -74,100 +63,80 @@ Example - Login with insecure registry from command line:
oras login --insecure localhost:5000
`,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
return preRunLogin(opts)
},
RunE: func(cmd *cobra.Command, args []string) error {
opts.hostname = args[0]
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")
cmd.Flags().BoolVarP(&opts.insecure, "insecure", "k", false, "allow connections to SSL registry without certs")
cmd.Flags().StringVarP(&opts.caFilePath, "ca-file", "", "", "server certificate authority file for the remote registry")
cmd.Flags().BoolVarP(&opts.plainHTTP, "plain-http", "", false, "allow insecure connections to registry without SSL")
cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", false, "verbose output")
option.ApplyFlags(&opts, cmd.Flags())
return cmd
}

func runLogin(opts loginOptions) (err error) {
var logLevel logrus.Level
if opts.debug {
logLevel = logrus.DebugLevel
} else if opts.verbose {
logLevel = logrus.InfoLevel
} else {
logLevel = logrus.WarnLevel
}
ctx, _ := trace.WithLoggerLevel(context.Background(), logLevel)

// Prepare auth client
store, err := credential.NewStore(opts.configs...)
if err != nil {
func preRunLogin(opts loginOptions) (err error) {
if err := opts.Credential.ReadPassword(); err != nil {
return err
}

// Prompt credential
if opts.fromStdin {
password, err := io.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 == "" {
if opts.Password == "" {
if opts.Username == "" {
// prompt for username
username, err := readLine("Username: ", false)
if err != nil {
return err
}
opts.username = strings.TrimSpace(username)
opts.Username = strings.TrimSpace(username)
}
if opts.username == "" {
if opts.password, err = readLine("Token: ", true); err != nil {
if opts.Username == "" {
// prompt for token
if opts.Password, err = readLine("Token: ", true); err != nil {
return err
} else if opts.password == "" {
} else if opts.Password == "" {
return errors.New("token required")
}
} else {
if opts.password, err = readLine("Password: ", true); err != nil {
// prompt for password
if opts.Password, err = readLine("Password: ", true); err != nil {
return err
} else if opts.password == "" {
} 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.")
}
return nil
}

func runLogin(opts loginOptions) (err error) {
ctx, _ := opts.SetLoggerLevel()
// Prepare auth client
store, err := credential.NewStore(opts.Configs...)
if err != nil {
return err
}
// Ping to ensure credential is valid
remote, err := remote.NewRegistry(opts.hostname)
remote, err := remote.NewRegistry(opts.Hostname)
if err != nil {
return err
}
remote.PlainHTTP = opts.plainHTTP
cred := credential.Credential(opts.username, opts.password)
rootCAs, err := http.LoadCertPool(opts.caFilePath)
remote.PlainHTTP = opts.PlainHTTP
cred := credential.Credential(opts.Username, opts.Password)
config, err := opts.TLS.Config()
if err != nil {
return err
}
remote.Client = http.NewClient(http.ClientOptions{
Credential: cred,
SkipTLSVerify: opts.insecure,
Debug: opts.debug,
RootCAs: rootCAs,
Credential: cred,
TLSConfig: config,
Debug: opts.Debug,
})
qweeah marked this conversation as resolved.
Show resolved Hide resolved
if err = remote.Ping(ctx); err != nil {
return err
}

// Store the validated credential
if err := store.Store(opts.hostname, cred); err != nil {
if err := store.Store(opts.Hostname, cred); err != nil {
return err
}

fmt.Println("Login Succeeded")
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.4.0
github.com/spf13/pflag v1.0.5
oras.land/oras-go v0.4.0
oras.land/oras-go/v2 v2.0.0-20220415135518-c34895e747dc
)
Expand Down Expand Up @@ -42,7 +43,6 @@ require (
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220405210540-1e041c57c461 // indirect
Expand Down
Loading