Refactor setup command and docs (#1253)

* Refactor setup command and docs

* Fix slice flags

* Fix
This commit is contained in:
Hidetake Iwata
2025-01-25 16:08:28 +09:00
committed by GitHub
parent 56e09ad65e
commit 3a38753ee7
11 changed files with 269 additions and 416 deletions

View File

@@ -8,6 +8,7 @@ import (
"time"
"github.com/int128/kubelogin/mocks/github.com/int128/kubelogin/pkg/usecases/credentialplugin_mock"
"github.com/int128/kubelogin/mocks/github.com/int128/kubelogin/pkg/usecases/setup_mock"
"github.com/int128/kubelogin/mocks/github.com/int128/kubelogin/pkg/usecases/standalone_mock"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/testing/logger"
@@ -16,6 +17,7 @@ import (
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/setup"
"github.com/int128/kubelogin/pkg/usecases/standalone"
)
@@ -23,6 +25,14 @@ func TestCmd_Run(t *testing.T) {
const executable = "kubelogin"
const version = "HEAD"
defaultGrantOptionSet := authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
}
t.Run("root", func(t *testing.T) {
tests := map[string]struct {
args []string
@@ -31,13 +41,7 @@ func TestCmd_Run(t *testing.T) {
"Defaults": {
args: []string{executable},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
GrantOptionSet: defaultGrantOptionSet,
},
},
"FullOptions": {
@@ -51,13 +55,7 @@ func TestCmd_Run(t *testing.T) {
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
GrantOptionSet: defaultGrantOptionSet,
},
},
}
@@ -122,13 +120,7 @@ func TestCmd_Run(t *testing.T) {
Directory: filepath.Join(userHomeDir, ".kube/cache/oidc-login"),
Storage: tokencache.StorageAuto,
},
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
GrantOptionSet: defaultGrantOptionSet,
},
},
"FullOptions": {
@@ -153,13 +145,7 @@ func TestCmd_Run(t *testing.T) {
Directory: filepath.Join(userHomeDir, ".kube/cache/oidc-login"),
Storage: tokencache.StorageDisk,
},
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
GrantOptionSet: defaultGrantOptionSet,
},
},
"AccessToken": {
@@ -179,13 +165,7 @@ func TestCmd_Run(t *testing.T) {
Directory: filepath.Join(userHomeDir, ".kube/cache/oidc-login"),
Storage: tokencache.StorageAuto,
},
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
GrantOptionSet: defaultGrantOptionSet,
},
},
"HomedirExpansion": {
@@ -282,4 +262,54 @@ func TestCmd_Run(t *testing.T) {
}
})
})
t.Run("setup", func(t *testing.T) {
t.Run("NoOption", func(t *testing.T) {
ctx := context.TODO()
cmd := Cmd{
Logger: logger.New(t),
Root: &Root{
Logger: logger.New(t),
},
}
exitCode := cmd.Run(ctx, []string{executable, "setup"}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("WithOptions", func(t *testing.T) {
ctx := context.TODO()
setupMock := setup_mock.NewMockInterface(t)
setupMock.EXPECT().Do(ctx, setup.Input{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT",
ExtraScopes: []string{"email", "profile"},
GrantOptionSet: defaultGrantOptionSet,
ChangedFlags: []string{
"--oidc-issuer-url=https://issuer.example.com",
"--oidc-client-id=YOUR_CLIENT",
"--oidc-extra-scope=email",
"--oidc-extra-scope=profile",
},
}).Return(nil)
cmd := Cmd{
Logger: logger.New(t),
Root: &Root{
Logger: logger.New(t),
},
Setup: &Setup{
Setup: setupMock,
},
}
exitCode := cmd.Run(ctx, []string{executable, "setup",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT",
"--oidc-extra-scope", "email,profile",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
})
}

View File

@@ -3,6 +3,8 @@ package cmd
import (
"fmt"
_ "embed"
"github.com/int128/kubelogin/pkg/usecases/setup"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -35,13 +37,31 @@ type Setup struct {
Setup setup.Interface
}
//go:embed setup.md
var setupLongDescription string
func (cmd *Setup) New() *cobra.Command {
var o setupOptions
c := &cobra.Command{
Use: "setup",
Short: "Show the setup instruction",
Long: setupLongDescription,
Args: cobra.NoArgs,
RunE: func(c *cobra.Command, _ []string) error {
var changedFlags []string
c.Flags().VisitAll(func(f *pflag.Flag) {
if !f.Changed {
return
}
if sliceValue, ok := f.Value.(pflag.SliceValue); ok {
for _, v := range sliceValue.GetSlice() {
changedFlags = append(changedFlags, fmt.Sprintf("--%s=%s", f.Name, v))
}
return
}
changedFlags = append(changedFlags, fmt.Sprintf("--%s=%s", f.Name, f.Value))
})
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return fmt.Errorf("setup: %w", err)
@@ -50,7 +70,7 @@ func (cmd *Setup) New() *cobra.Command {
if err != nil {
return fmt.Errorf("setup: %w", err)
}
in := setup.Stage2Input{
in := setup.Input{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
@@ -59,18 +79,12 @@ func (cmd *Setup) New() *cobra.Command {
PKCEMethod: pkceMethod,
GrantOptionSet: grantOptionSet,
TLSClientConfig: o.tlsOptions.tlsClientConfig(),
}
if c.Flags().Lookup("listen-address").Changed {
in.ListenAddressArgs = o.authenticationOptions.ListenAddress
}
if c.Flags().Lookup("oidc-pkce-method").Changed {
in.PKCEMethodArg = o.pkceOptions.PKCEMethod
ChangedFlags: changedFlags,
}
if in.IssuerURL == "" || in.ClientID == "" {
cmd.Setup.DoStage1()
return nil
return c.Help()
}
if err := cmd.Setup.DoStage2(c.Context(), in); err != nil {
if err := cmd.Setup.Do(c.Context(), in); err != nil {
return fmt.Errorf("setup: %w", err)
}
return nil

12
pkg/cmd/setup.md Normal file
View File

@@ -0,0 +1,12 @@
This setup shows the instruction of Kubernetes OpenID Connect authentication.
You need to set up the OpenID Connect Provider.
Run the following command to authenticate with the OpenID Connect Provider:
```
kubectl oidc-login setup \
--oidc-issuer-url=ISSUER_URL \
--oidc-client-id=YOUR_CLIENT_ID
```
See https://github.com/int128/kubelogin for the details.

View File

@@ -3,9 +3,17 @@ package setup
import (
"context"
"fmt"
"strconv"
"strings"
"text/template"
_ "embed"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
@@ -15,11 +23,62 @@ var Set = wire.NewSet(
)
type Interface interface {
DoStage1()
DoStage2(ctx context.Context, in Stage2Input) error
Do(ctx context.Context, in Input) error
}
type Setup struct {
Authentication authentication.Interface
Logger logger.Interface
}
//go:embed setup.md
var setupMarkdown string
var setupTemplate = template.Must(template.New("setup.md").Funcs(template.FuncMap{
"quote": strconv.Quote,
}).Parse(setupMarkdown))
type Input struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string
UseAccessToken bool
PKCEMethod oidc.PKCEMethod
GrantOptionSet authentication.GrantOptionSet
TLSClientConfig tlsclientconfig.Config
ChangedFlags []string
}
func (u Setup) Do(ctx context.Context, in Input) error {
u.Logger.Printf("Authentication in progress...")
out, err := u.Authentication.Do(ctx, authentication.Input{
Provider: oidc.Provider{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
PKCEMethod: in.PKCEMethod,
UseAccessToken: in.UseAccessToken,
},
GrantOptionSet: in.GrantOptionSet,
TLSClientConfig: in.TLSClientConfig,
})
if err != nil {
return fmt.Errorf("authentication error: %w", err)
}
idTokenClaims, err := out.TokenSet.DecodeWithoutVerify()
if err != nil {
return fmt.Errorf("you got an invalid token: %w", err)
}
var b strings.Builder
if err := setupTemplate.Execute(&b, map[string]any{
"IDTokenPrettyJSON": idTokenClaims.Pretty,
"Flags": in.ChangedFlags,
}); err != nil {
return fmt.Errorf("render the template: %w", err)
}
u.Logger.Printf(b.String())
return nil
}

View File

@@ -0,0 +1,24 @@
## Authenticated with the OpenID Connect Provider
You got the token with the following claims:
```
{{ .IDTokenPrettyJSON }}
```
## Set up the kubeconfig
You can run the following command to set up the kubeconfig:
```
kubectl config set-credentials oidc \
--exec-api-version=client.authentication.k8s.io/v1 \
--exec-interactive-mode=Never \
--exec-command=kubectl \
--exec-arg=oidc-login \
--exec-arg=get-token \
{{- range $index, $flag := .Flags }}
{{- if $index}} \{{end}}
--exec-arg={{ $flag | quote }}
{{- end }}
```

View File

@@ -0,0 +1,66 @@
package setup
import (
"context"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/int128/kubelogin/mocks/github.com/int128/kubelogin/pkg/usecases/authentication_mock"
"github.com/int128/kubelogin/pkg/oidc"
testingJWT "github.com/int128/kubelogin/pkg/testing/jwt"
"github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
func TestSetup_Do(t *testing.T) {
issuedIDToken := testingJWT.EncodeF(t, func(claims *testingJWT.Claims) {
claims.Issuer = "https://issuer.example.com"
claims.Subject = "YOUR_SUBJECT"
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(1 * time.Hour))
})
dummyTLSClientConfig := tlsclientconfig.Config{
CACertFilename: []string{"/path/to/cert"},
}
var grantOptionSet authentication.GrantOptionSet
ctx := context.Background()
in := Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
GrantOptionSet: grantOptionSet,
TLSClientConfig: dummyTLSClientConfig,
ChangedFlags: []string{
"--oidc-issuer-url=https://accounts.google.com",
"--oidc-client-id=YOUR_CLIENT_ID",
},
}
mockAuthentication := authentication_mock.NewMockInterface(t)
mockAuthentication.EXPECT().
Do(ctx, authentication.Input{
Provider: oidc.Provider{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
},
GrantOptionSet: grantOptionSet,
TLSClientConfig: dummyTLSClientConfig,
}).
Return(&authentication.Output{
TokenSet: oidc.TokenSet{
IDToken: issuedIDToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
},
}, nil)
u := Setup{
Authentication: mockAuthentication,
Logger: logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
}
}

View File

@@ -1,28 +0,0 @@
package setup
const stage1 = `This setup shows the instruction of Kubernetes OpenID Connect authentication.
See also https://github.com/int128/kubelogin.
## 1. Set up the OpenID Connect Provider
Open the OpenID Connect Provider and create a client.
For example, Google Identity Platform:
Open https://console.developers.google.com/apis/credentials and create an OAuth client of "Other" type.
ISSUER is https://accounts.google.com
## 2. Verify authentication
Run the following command to proceed.
kubectl oidc-login setup \
--oidc-issuer-url=ISSUER \
--oidc-client-id=YOUR_CLIENT_ID \
--oidc-client-secret=YOUR_CLIENT_SECRET
You can set your CA certificate. See also the options by --help.
`
func (u *Setup) DoStage1() {
u.Logger.Printf(stage1)
}

View File

@@ -1,178 +0,0 @@
package setup
import (
"context"
"fmt"
"path/filepath"
"strings"
"text/template"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/int128/kubelogin/pkg/usecases/authentication"
)
var stage2Tpl = template.Must(template.New("").Parse(`
## 2. Verify authentication
You got a token with the following claims:
{{ .IDTokenPrettyJSON }}
## 3. Bind a cluster role
Run the following command:
kubectl create clusterrolebinding oidc-cluster-admin --clusterrole=cluster-admin --user='{{ .IssuerURL }}#{{ .Subject }}'
## 4. Set up the Kubernetes API server
Add the following options to the kube-apiserver:
--oidc-issuer-url={{ .IssuerURL }}
--oidc-client-id={{ .ClientID }}
## 5. Set up the kubeconfig
Run the following command:
kubectl config set-credentials oidc \
--exec-api-version=client.authentication.k8s.io/v1 \
--exec-command=kubectl \
--exec-arg=oidc-login \
--exec-arg=get-token \
{{- range $index, $arg := .Args }}
{{- if $index}} \{{end}}
--exec-arg={{ $arg }}
{{- end }}
## 6. Verify cluster access
Make sure you can access the Kubernetes cluster.
kubectl --user=oidc get nodes
You can switch the default context to oidc.
kubectl config set-context --current --user=oidc
You can share the kubeconfig to your team members for on-boarding.
`))
type stage2Vars struct {
IDTokenPrettyJSON string
IssuerURL string
ClientID string
Args []string
Subject string
}
// Stage2Input represents an input DTO of the stage2.
type Stage2Input struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
UseAccessToken bool // optional
ListenAddressArgs []string // non-nil if set by the command arg
PKCEMethod oidc.PKCEMethod
PKCEMethodArg string
GrantOptionSet authentication.GrantOptionSet
TLSClientConfig tlsclientconfig.Config
}
func (u *Setup) DoStage2(ctx context.Context, in Stage2Input) error {
u.Logger.Printf("authentication in progress...")
out, err := u.Authentication.Do(ctx, authentication.Input{
Provider: oidc.Provider{
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
ExtraScopes: in.ExtraScopes,
PKCEMethod: in.PKCEMethod,
UseAccessToken: in.UseAccessToken,
},
GrantOptionSet: in.GrantOptionSet,
TLSClientConfig: in.TLSClientConfig,
})
if err != nil {
return fmt.Errorf("authentication error: %w", err)
}
idTokenClaims, err := out.TokenSet.DecodeWithoutVerify()
if err != nil {
return fmt.Errorf("you got an invalid token: %w", err)
}
v := stage2Vars{
IDTokenPrettyJSON: idTokenClaims.Pretty,
IssuerURL: in.IssuerURL,
ClientID: in.ClientID,
Args: makeCredentialPluginArgs(in),
Subject: idTokenClaims.Subject,
}
var b strings.Builder
if err := stage2Tpl.Execute(&b, &v); err != nil {
return fmt.Errorf("could not render the template: %w", err)
}
u.Logger.Printf(b.String())
return nil
}
func makeCredentialPluginArgs(in Stage2Input) []string {
var args []string
args = append(args, "--oidc-issuer-url="+in.IssuerURL)
args = append(args, "--oidc-client-id="+in.ClientID)
if in.ClientSecret != "" {
args = append(args, "--oidc-client-secret="+in.ClientSecret)
}
for _, extraScope := range in.ExtraScopes {
args = append(args, "--oidc-extra-scope="+extraScope)
}
if in.PKCEMethodArg != "" {
args = append(args, "--oidc-pkce-method="+in.PKCEMethodArg)
}
if in.UseAccessToken {
args = append(args, "--oidc-use-access-token")
}
for _, f := range in.TLSClientConfig.CACertFilename {
args = append(args, "--certificate-authority="+f)
}
for _, d := range in.TLSClientConfig.CACertData {
args = append(args, "--certificate-authority-data="+d)
}
if in.TLSClientConfig.SkipTLSVerify {
args = append(args, "--insecure-skip-tls-verify")
}
if in.GrantOptionSet.AuthCodeBrowserOption != nil {
if in.GrantOptionSet.AuthCodeBrowserOption.SkipOpenBrowser {
args = append(args, "--skip-open-browser")
}
if in.GrantOptionSet.AuthCodeBrowserOption.BrowserCommand != "" {
args = append(args, "--browser-command="+in.GrantOptionSet.AuthCodeBrowserOption.BrowserCommand)
}
if in.GrantOptionSet.AuthCodeBrowserOption.LocalServerCertFile != "" {
// Resolve the absolute path for the cert files so the user doesn't have to know
// to use one when running setup.
certpath, err := filepath.Abs(in.GrantOptionSet.AuthCodeBrowserOption.LocalServerCertFile)
if err != nil {
panic(err)
}
keypath, err := filepath.Abs(in.GrantOptionSet.AuthCodeBrowserOption.LocalServerKeyFile)
if err != nil {
panic(err)
}
args = append(args, "--local-server-cert="+certpath)
args = append(args, "--local-server-key="+keypath)
}
}
for _, l := range in.ListenAddressArgs {
args = append(args, "--listen-address="+l)
}
if in.GrantOptionSet.ROPCOption != nil {
if in.GrantOptionSet.ROPCOption.Username != "" {
args = append(args, "--username="+in.GrantOptionSet.ROPCOption.Username)
}
}
return args
}

View File

@@ -1,111 +0,0 @@
package setup
import (
"context"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/int128/kubelogin/mocks/github.com/int128/kubelogin/pkg/usecases/authentication_mock"
"github.com/int128/kubelogin/pkg/oidc"
testingJWT "github.com/int128/kubelogin/pkg/testing/jwt"
"github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"github.com/stretchr/testify/assert"
)
func TestSetup_DoStage2(t *testing.T) {
issuedIDToken := testingJWT.EncodeF(t, func(claims *testingJWT.Claims) {
claims.Issuer = "https://issuer.example.com"
claims.Subject = "YOUR_SUBJECT"
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(1 * time.Hour))
})
dummyTLSClientConfig := tlsclientconfig.Config{
CACertFilename: []string{"/path/to/cert"},
}
var grantOptionSet authentication.GrantOptionSet
ctx := context.Background()
in := Stage2Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
GrantOptionSet: grantOptionSet,
TLSClientConfig: dummyTLSClientConfig,
}
mockAuthentication := authentication_mock.NewMockInterface(t)
mockAuthentication.EXPECT().
Do(ctx, authentication.Input{
Provider: oidc.Provider{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email"},
},
GrantOptionSet: grantOptionSet,
TLSClientConfig: dummyTLSClientConfig,
}).
Return(&authentication.Output{
TokenSet: oidc.TokenSet{
IDToken: issuedIDToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
},
}, nil)
u := Setup{
Authentication: mockAuthentication,
Logger: logger.New(t),
}
if err := u.DoStage2(ctx, in); err != nil {
t.Errorf("DoStage2 returned error: %+v", err)
}
}
func Test_makeCredentialPluginArgs(t *testing.T) {
in := Stage2Input{
IssuerURL: "https://oidc.example.com",
ClientID: "test_kid",
ClientSecret: "test_ksecret",
ExtraScopes: []string{"groups"},
PKCEMethodArg: "S256",
ListenAddressArgs: []string{"127.0.0.1:8080", "127.0.0.1:8888"},
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
SkipOpenBrowser: true,
BrowserCommand: "firefox",
LocalServerCertFile: "/path/to/cert.crt",
LocalServerKeyFile: "/path/to/cert.key",
},
ROPCOption: &ropc.Option{
Username: "user1",
},
},
TLSClientConfig: tlsclientconfig.Config{
CACertFilename: []string{"/path/to/ca.crt"},
CACertData: []string{"base64encoded1"},
SkipTLSVerify: true,
},
}
expet := []string{
"--oidc-issuer-url=https://oidc.example.com",
"--oidc-client-id=test_kid",
"--oidc-client-secret=test_ksecret",
"--oidc-extra-scope=groups",
"--oidc-pkce-method=S256",
"--certificate-authority=/path/to/ca.crt",
"--certificate-authority-data=base64encoded1",
"--insecure-skip-tls-verify",
"--skip-open-browser",
"--browser-command=firefox",
"--local-server-cert=/path/to/cert.crt",
"--local-server-key=/path/to/cert.key",
"--listen-address=127.0.0.1:8080",
"--listen-address=127.0.0.1:8888",
"--username=user1",
}
got := makeCredentialPluginArgs(in)
assert.Equal(t, expet, got)
}