* Refactor: template rendering

* Refactor: rename DecodedIDToken fields

* Refactor: expand command options

* Refactor: improve help messages
This commit is contained in:
Hidetake Iwata
2019-09-30 18:27:23 +09:00
committed by GitHub
parent 8c640f6c73
commit 0bc117ddc7
10 changed files with 91 additions and 97 deletions

View File

@@ -82,21 +82,21 @@ If you are looking for a specific version, see [the release tags](https://github
Kubelogin supports the following options: Kubelogin supports the following options:
``` ```
% kubelogin get-token -h % kubectl oidc-login get-token -h
Run as a kubectl credential plugin Run as a kubectl credential plugin
Usage: Usage:
kubelogin get-token [flags] kubelogin get-token [flags]
Flags: Flags:
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
--oidc-issuer-url string Issuer URL of the provider (mandatory) --oidc-issuer-url string Issuer URL of the provider (mandatory)
--oidc-client-id string Client ID of the provider (mandatory) --oidc-client-id string Client ID of the provider (mandatory)
--oidc-client-secret string Client secret of the provider --oidc-client-secret string Client secret of the provider
--oidc-extra-scope strings Scopes to request to the provider --oidc-extra-scope strings Scopes to request to the provider
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
--certificate-authority string Path to a cert file for the certificate authority --certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login") --token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")

View File

@@ -78,38 +78,36 @@ If the refresh token has expired, kubelogin will proceed the authentication.
Kubelogin supports the following options: Kubelogin supports the following options:
``` ```
% kubelogin -h % kubectl oidc-login -h
Login to the OpenID Connect provider and update the kubeconfig Login to the OpenID Connect provider.
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
Run the following command to show the setup instruction:
kubectl oidc-login setup
See https://github.com/int128/kubelogin for more.
Usage: Usage:
kubelogin [flags] main [flags]
kubelogin [command] main [command]
Examples:
# Login to the provider using the authorization code flow.
kubelogin
# Login to the provider using the resource owner password credentials flow.
kubelogin --username USERNAME --password PASSWORD
# Run as a credential plugin.
kubelogin get-token --oidc-issuer-url=https://issuer.example.com
Available Commands: Available Commands:
get-token Run as a kubectl credential plugin get-token Run as a kubectl credential plugin
help Help about any command help Help about any command
setup Show the setup instruction
version Print the version information version Print the version information
Flags: Flags:
--kubeconfig string Path to the kubeconfig file --kubeconfig string Path to the kubeconfig file
--context string The name of the kubeconfig context to use --context string The name of the kubeconfig context to use
--user string The name of the kubeconfig user to use. Prior to --context --user string The name of the kubeconfig user to use. Prior to --context
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000]) --listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication --skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant --username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it --password string If set, use the password instead of asking it
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--add_dir_header If true, adds the file directory to the header --add_dir_header If true, adds the file directory to the header
--alsologtostderr log to standard error as well as files --alsologtostderr log to standard error as well as files
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
@@ -122,8 +120,8 @@ Flags:
--stderrthreshold severity logs at or above this threshold go to stderr (default 2) --stderrthreshold severity logs at or above this threshold go to stderr (default 2)
-v, --v Level number for the log level verbosity -v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
-h, --help help for kubelogin -h, --help help for main
--version version for kubelogin --version version for main
``` ```
### Kubeconfig ### Kubeconfig

View File

@@ -23,15 +23,6 @@ type Interface interface {
Run(ctx context.Context, args []string, version string) int Run(ctx context.Context, args []string, version string) int
} }
const examples = ` # Login to the provider using the authorization code flow.
%[1]s
# Login to the provider using the resource owner password credentials flow.
%[1]s --username USERNAME --password PASSWORD
# Run as a credential plugin.
%[1]s get-token --oidc-issuer-url=https://issuer.example.com`
var defaultListenPort = []int{8000, 18000} var defaultListenPort = []int{8000, 18000}
var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login" var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login"

View File

@@ -12,24 +12,29 @@ import (
// getTokenOptions represents the options for get-token command. // getTokenOptions represents the options for get-token command.
type getTokenOptions struct { type getTokenOptions struct {
loginOptions
IssuerURL string IssuerURL string
ClientID string ClientID string
ClientSecret string ClientSecret string
ExtraScopes []string ExtraScopes []string
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
CertificateAuthority string CertificateAuthority string
SkipTLSVerify bool SkipTLSVerify bool
Verbose int
TokenCacheDir string TokenCacheDir string
} }
func (o *getTokenOptions) register(f *pflag.FlagSet) { func (o *getTokenOptions) register(f *pflag.FlagSet) {
f.SortFlags = false f.SortFlags = false
o.loginOptions.register(f)
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider (mandatory)") f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider (mandatory)")
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider (mandatory)") f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider (mandatory)")
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider") f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider") f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority") f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure") f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens") f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")

View File

@@ -2,7 +2,6 @@ package cmd
import ( import (
"context" "context"
"fmt"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig" "github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger" "github.com/int128/kubelogin/pkg/adaptors/logger"
@@ -12,38 +11,40 @@ import (
"golang.org/x/xerrors" "golang.org/x/xerrors"
) )
// kubectlOptions represents kubectl specific options. const longDescription = `Login to the OpenID Connect provider.
type kubectlOptions struct {
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
Run the following command to show the setup instruction:
kubectl oidc-login setup
See https://github.com/int128/kubelogin for more.
`
// rootOptions represents the options for the root command.
type rootOptions struct {
Kubeconfig string Kubeconfig string
Context string Context string
User string User string
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
CertificateAuthority string CertificateAuthority string
SkipTLSVerify bool SkipTLSVerify bool
} }
func (o *kubectlOptions) register(f *pflag.FlagSet) { func (o *rootOptions) register(f *pflag.FlagSet) {
f.SortFlags = false f.SortFlags = false
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file") f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use") f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context") f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
}
// loginOptions represents the options for Login use-case.
type loginOptions struct {
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
}
func (o *loginOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order") f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication") f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant") f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it") f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
} }
type Root struct { type Root struct {
@@ -52,15 +53,12 @@ type Root struct {
} }
func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command { func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command {
var o struct { var o rootOptions
kubectlOptions
loginOptions
}
rootCmd := &cobra.Command{ rootCmd := &cobra.Command{
Use: executable, Use: executable,
Short: "Login to the OpenID Connect provider and update the kubeconfig", Short: "Login to the OpenID Connect provider",
Example: fmt.Sprintf(examples, executable), Long: longDescription,
Args: cobra.NoArgs, Args: cobra.NoArgs,
RunE: func(*cobra.Command, []string) error { RunE: func(*cobra.Command, []string) error {
in := standalone.Input{ in := standalone.Input{
KubeconfigFilename: o.Kubeconfig, KubeconfigFilename: o.Kubeconfig,
@@ -79,8 +77,7 @@ func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command {
return nil return nil
}, },
} }
o.kubectlOptions.register(rootCmd.Flags()) o.register(rootCmd.Flags())
o.loginOptions.register(rootCmd.Flags())
cmd.Logger.AddFlags(rootCmd.PersistentFlags()) cmd.Logger.AddFlags(rootCmd.PersistentFlags())
return rootCmd return rootCmd
} }

View File

@@ -16,9 +16,9 @@ type DecoderInterface interface {
} }
type DecodedIDToken struct { type DecodedIDToken struct {
IDTokenSubject string Subject string
IDTokenExpiry time.Time Expiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging Claims map[string]string // string representation of claims for logging
} }
type Decoder struct{} type Decoder struct{}
@@ -43,9 +43,9 @@ func (d *Decoder) DecodeIDToken(t string) (*DecodedIDToken, error) {
return nil, xerrors.Errorf("could not decode the json of token: %w", err) return nil, xerrors.Errorf("could not decode the json of token: %w", err)
} }
return &DecodedIDToken{ return &DecodedIDToken{
IDTokenSubject: claims.Subject, Subject: claims.Subject,
IDTokenExpiry: time.Unix(claims.ExpiresAt, 0), Expiry: time.Unix(claims.ExpiresAt, 0),
IDTokenClaims: dumpRawClaims(rawClaims), Claims: dumpRawClaims(rawClaims),
}, nil }, nil
} }

View File

@@ -21,10 +21,10 @@ func TestDecoder_DecodeIDToken(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("DecodeIDToken error: %s", err) t.Fatalf("DecodeIDToken error: %s", err)
} }
if decodedToken.IDTokenExpiry != expiry { if decodedToken.Expiry != expiry {
t.Errorf("IDTokenExpiry wants %s but %s", expiry, decodedToken.IDTokenExpiry) t.Errorf("Expiry wants %s but %s", expiry, decodedToken.Expiry)
} }
t.Logf("IDTokenClaims=%+v", decodedToken.IDTokenClaims) t.Logf("Claims=%+v", decodedToken.Claims)
}) })
t.Run("InvalidToken", func(t *testing.T) { t.Run("InvalidToken", func(t *testing.T) {
decodedToken, err := decoder.DecodeIDToken("HEADER.INVALID_TOKEN.SIGNATURE") decodedToken, err := decoder.DecodeIDToken("HEADER.INVALID_TOKEN.SIGNATURE")

View File

@@ -85,18 +85,18 @@ func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
if err != nil { if err != nil {
return nil, xerrors.Errorf("invalid token and you need to remove the cache: %w", err) return nil, xerrors.Errorf("invalid token and you need to remove the cache: %w", err)
} }
if token.IDTokenExpiry.After(time.Now()) { //TODO: inject time service if token.Expiry.After(time.Now()) { //TODO: inject time service
u.Logger.V(1).Infof("you already have a valid token until %s", token.IDTokenExpiry) u.Logger.V(1).Infof("you already have a valid token until %s", token.Expiry)
return &Output{ return &Output{
AlreadyHasValidIDToken: true, AlreadyHasValidIDToken: true,
IDToken: in.OIDCConfig.IDToken, IDToken: in.OIDCConfig.IDToken,
RefreshToken: in.OIDCConfig.RefreshToken, RefreshToken: in.OIDCConfig.RefreshToken,
IDTokenSubject: token.IDTokenSubject, IDTokenSubject: token.Subject,
IDTokenExpiry: token.IDTokenExpiry, IDTokenExpiry: token.Expiry,
IDTokenClaims: token.IDTokenClaims, IDTokenClaims: token.Claims,
}, nil }, nil
} }
u.Logger.V(1).Infof("you have an expired token at %s", token.IDTokenExpiry) u.Logger.V(1).Infof("you have an expired token at %s", token.Expiry)
} }
u.Logger.V(1).Infof("initializing an OIDCFactory client") u.Logger.V(1).Infof("initializing an OIDCFactory client")

View File

@@ -274,9 +274,9 @@ func TestAuthentication_Do(t *testing.T) {
mockOIDCDecoder.EXPECT(). mockOIDCDecoder.EXPECT().
DecodeIDToken("VALID_ID_TOKEN"). DecodeIDToken("VALID_ID_TOKEN").
Return(&oidc.DecodedIDToken{ Return(&oidc.DecodedIDToken{
IDTokenSubject: "YOUR_SUBJECT", Subject: "YOUR_SUBJECT",
IDTokenExpiry: futureTime, Expiry: futureTime,
IDTokenClaims: dummyTokenClaims, Claims: dummyTokenClaims,
}, nil) }, nil)
u := Authentication{ u := Authentication{
OIDCFactory: mock_oidc.NewMockFactoryInterface(ctrl), OIDCFactory: mock_oidc.NewMockFactoryInterface(ctrl),
@@ -315,9 +315,9 @@ func TestAuthentication_Do(t *testing.T) {
mockOIDCDecoder.EXPECT(). mockOIDCDecoder.EXPECT().
DecodeIDToken("EXPIRED_ID_TOKEN"). DecodeIDToken("EXPIRED_ID_TOKEN").
Return(&oidc.DecodedIDToken{ Return(&oidc.DecodedIDToken{
IDTokenSubject: "YOUR_SUBJECT", Subject: "YOUR_SUBJECT",
IDTokenExpiry: pastTime, Expiry: pastTime,
IDTokenClaims: dummyTokenClaims, Claims: dummyTokenClaims,
}, nil) }, nil)
mockOIDCClient := mock_oidc.NewMockInterface(ctrl) mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT(). mockOIDCClient.EXPECT().
@@ -373,9 +373,9 @@ func TestAuthentication_Do(t *testing.T) {
mockOIDCDecoder.EXPECT(). mockOIDCDecoder.EXPECT().
DecodeIDToken("EXPIRED_ID_TOKEN"). DecodeIDToken("EXPIRED_ID_TOKEN").
Return(&oidc.DecodedIDToken{ Return(&oidc.DecodedIDToken{
IDTokenSubject: "YOUR_SUBJECT", Subject: "YOUR_SUBJECT",
IDTokenExpiry: pastTime, Expiry: pastTime,
IDTokenClaims: dummyTokenClaims, Claims: dummyTokenClaims,
}, nil) }, nil)
mockOIDCClient := mock_oidc.NewMockInterface(ctrl) mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT(). mockOIDCClient.EXPECT().

View File

@@ -37,10 +37,8 @@ type Input struct {
SkipTLSVerify bool SkipTLSVerify bool
} }
const oidcConfigErrorMessage = `NOTE: const oidcConfigErrorMessage = `You need to set up the kubeconfig for OpenID Connect authentication.
You need to setup the kubeconfig for OpenID Connect authentication.
See https://github.com/int128/kubelogin for more. See https://github.com/int128/kubelogin for more.
` `
// Standalone provides the use case of explicit login. // Standalone provides the use case of explicit login.
@@ -99,13 +97,13 @@ func (u *Standalone) Do(ctx context.Context, in Input) error {
return nil return nil
} }
var deprecation = template.Must(template.New("").Parse( var deprecationTpl = template.Must(template.New("").Parse(
`IMPORTANT NOTICE: `IMPORTANT NOTICE:
The credential plugin mode is available since v1.14.0. The credential plugin mode is available since v1.14.0.
Kubectl will automatically run kubelogin and you do not need to run kubelogin explicitly. Kubectl will automatically run kubelogin and you do not need to run kubelogin explicitly.
You can switch to the credential plugin mode by setting the following user to You can switch to the credential plugin mode by setting the following user to
{{ .kubeconfig }}. {{ .Kubeconfig }}.
--- ---
users: users:
- name: oidc - name: oidc
@@ -116,7 +114,7 @@ users:
args: args:
- oidc-login - oidc-login
- get-token - get-token
{{- range .args }} {{- range .Args }}
- {{ . }} - {{ . }}
{{- end }} {{- end }}
--- ---
@@ -124,6 +122,11 @@ See https://github.com/int128/kubelogin for more.
`)) `))
type deprecationVars struct {
Kubeconfig string
Args []string
}
func (u *Standalone) showDeprecation(in Input, p *kubeconfig.AuthProvider) error { func (u *Standalone) showDeprecation(in Input, p *kubeconfig.AuthProvider) error {
var args []string var args []string
args = append(args, "--oidc-issuer-url="+p.OIDCConfig.IDPIssuerURL) args = append(args, "--oidc-issuer-url="+p.OIDCConfig.IDPIssuerURL)
@@ -144,12 +147,12 @@ func (u *Standalone) showDeprecation(in Input, p *kubeconfig.AuthProvider) error
args = append(args, "--username="+in.Username) args = append(args, "--username="+in.Username)
} }
m := map[string]interface{}{ v := deprecationVars{
"kubeconfig": p.LocationOfOrigin, Kubeconfig: p.LocationOfOrigin,
"args": args, Args: args,
} }
var b strings.Builder var b strings.Builder
if err := deprecation.ExecuteTemplate(&b, "", m); err != nil { if err := deprecationTpl.Execute(&b, &v); err != nil {
return xerrors.Errorf("could not render the template: %w", err) return xerrors.Errorf("could not render the template: %w", err)
} }
u.Logger.Printf("%s", b.String()) u.Logger.Printf("%s", b.String())