Skip to content

Commit

Permalink
Infer apiVersion from KUBERNETES_EXEC_INFO environment variable (#1162
Browse files Browse the repository at this point in the history
)

* Infer apiVersion from KUBERNETES_EXEC_INFO

* Test client.authentication.k8s.io/v1

* Set --exec-interactive-mode

* Set --exec-interactive-mode=Never

* Fix comments
  • Loading branch information
int128 authored Nov 3, 2024
1 parent f1f2a37 commit 0e9a39a
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 57 deletions.

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

39 changes: 39 additions & 0 deletions pkg/credentialplugin/reader/reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Package reader provides a loader for the credential plugin.
package reader

import (
"encoding/json"
"fmt"
"os"

"github.com/google/wire"
"github.com/int128/kubelogin/pkg/credentialplugin"
"k8s.io/client-go/pkg/apis/clientauthentication"
)

var Set = wire.NewSet(
wire.Struct(new(Reader), "*"),
wire.Bind(new(Interface), new(*Reader)),
)

type Interface interface {
Read() (credentialplugin.Input, error)
}

type Reader struct{}

// Read parses the environment variable KUBERNETES_EXEC_INFO.
// If the environment variable is not given by kubectl, Read returns a zero value.
func (r Reader) Read() (credentialplugin.Input, error) {
execInfo := os.Getenv("KUBERNETES_EXEC_INFO")
if execInfo == "" {
return credentialplugin.Input{}, nil
}
var execCredential clientauthentication.ExecCredential
if err := json.Unmarshal([]byte(execInfo), &execCredential); err != nil {
return credentialplugin.Input{}, fmt.Errorf("invalid KUBERNETES_EXEC_INFO: %w", err)
}
return credentialplugin.Input{
ClientAuthenticationAPIVersion: execCredential.APIVersion,
}, nil
}
44 changes: 44 additions & 0 deletions pkg/credentialplugin/reader/reader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package reader

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/credentialplugin"
)

func TestReader_Read(t *testing.T) {
var reader Reader

t.Run("KUBERNETES_EXEC_INFO is empty", func(t *testing.T) {
input, err := reader.Read()
if err != nil {
t.Errorf("Read returned error: %v", err)
}
want := credentialplugin.Input{}
if diff := cmp.Diff(want, input); diff != "" {
t.Errorf("input mismatch (-want +got):\n%s", diff)
}
})
t.Run("KUBERNETES_EXEC_INFO is invalid JSON", func(t *testing.T) {
t.Setenv("KUBERNETES_EXEC_INFO", "invalid")
_, err := reader.Read()
if err == nil {
t.Errorf("Read wants error but no error")
}
})
t.Run("KUBERNETES_EXEC_INFO is v1", func(t *testing.T) {
t.Setenv(
"KUBERNETES_EXEC_INFO",
`{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1","spec":{"interactive":true}}`,
)
input, err := reader.Read()
if err != nil {
t.Errorf("Read returned error: %v", err)
}
want := credentialplugin.Input{ClientAuthenticationAPIVersion: "client.authentication.k8s.io/v1"}
if diff := cmp.Diff(want, input); diff != "" {
t.Errorf("input mismatch (-want +got):\n%s", diff)
}
})
}
11 changes: 9 additions & 2 deletions pkg/credentialplugin/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ package credentialplugin

import "time"

// Input represents an input object of the credential plugin.
// This may be a zero value if the input is not available.
type Input struct {
ClientAuthenticationAPIVersion string
}

// Output represents an output object of the credential plugin.
type Output struct {
Token string
Expiry time.Time
Token string
Expiry time.Time
ClientAuthenticationAPIVersion string
}
52 changes: 39 additions & 13 deletions pkg/credentialplugin/writer/credential_plugin.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Package writer provides a writer for a credential plugin.
// Package writer provides a writer for the credential plugin.
package writer

import (
Expand All @@ -9,6 +9,7 @@ import (
"github.com/int128/kubelogin/pkg/credentialplugin"
"github.com/int128/kubelogin/pkg/infrastructure/stdio"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
)

Expand All @@ -27,19 +28,44 @@ type Writer struct {

// Write writes the ExecCredential to standard output for kubectl.
func (w *Writer) Write(out credentialplugin.Output) error {
ec := &clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1beta1",
Kind: "ExecCredential",
},
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
Token: out.Token,
ExpirationTimestamp: &metav1.Time{Time: out.Expiry},
},
execCredential, err := generateExecCredential(out)
if err != nil {
return fmt.Errorf("generate ExecCredential: %w", err)
}
e := json.NewEncoder(w.Stdout)
if err := e.Encode(ec); err != nil {
return fmt.Errorf("could not write the ExecCredential: %w", err)
if err := json.NewEncoder(w.Stdout).Encode(execCredential); err != nil {
return fmt.Errorf("write ExecCredential: %w", err)
}
return nil
}

func generateExecCredential(out credentialplugin.Output) (any, error) {
switch out.ClientAuthenticationAPIVersion {
// If the API version is not available, fall back to v1beta1.
case clientauthenticationv1beta1.SchemeGroupVersion.String(), "":
return &clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: clientauthenticationv1beta1.SchemeGroupVersion.String(),
Kind: "ExecCredential",
},
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
Token: out.Token,
ExpirationTimestamp: &metav1.Time{Time: out.Expiry},
},
}, nil

case clientauthenticationv1.SchemeGroupVersion.String():
return &clientauthenticationv1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: clientauthenticationv1.SchemeGroupVersion.String(),
Kind: "ExecCredential",
},
Status: &clientauthenticationv1.ExecCredentialStatus{
Token: out.Token,
ExpirationTimestamp: &metav1.Time{Time: out.Expiry},
},
}, nil

default:
return nil, fmt.Errorf("unknown apiVersion: %s", out.ClientAuthenticationAPIVersion)
}
}
6 changes: 4 additions & 2 deletions pkg/di/di.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/cmd"
"github.com/int128/kubelogin/pkg/credentialplugin/writer"
credentialpluginreader "github.com/int128/kubelogin/pkg/credentialplugin/reader"
credentialpluginwriter "github.com/int128/kubelogin/pkg/credentialplugin/writer"
"github.com/int128/kubelogin/pkg/infrastructure/browser"
"github.com/int128/kubelogin/pkg/infrastructure/clock"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
Expand Down Expand Up @@ -55,7 +56,8 @@ func NewCmdForHeadless(clock.Interface, stdio.Stdin, stdio.Stdout, logger.Interf
repository.Set,
client.Set,
loader.Set,
writer.Set,
credentialpluginreader.Set,
credentialpluginwriter.Set,
)
return nil
}
13 changes: 8 additions & 5 deletions pkg/di/wire_gen.go

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

34 changes: 22 additions & 12 deletions pkg/usecases/credentialplugin/get_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import (

"github.com/google/wire"
"github.com/int128/kubelogin/pkg/credentialplugin"
"github.com/int128/kubelogin/pkg/credentialplugin/writer"
credentialpluginreader "github.com/int128/kubelogin/pkg/credentialplugin/reader"
credentialpluginwriter "github.com/int128/kubelogin/pkg/credentialplugin/writer"
"github.com/int128/kubelogin/pkg/infrastructure/clock"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/oidc"
Expand Down Expand Up @@ -38,16 +39,23 @@ type Input struct {
}

type GetToken struct {
Authentication authentication.Interface
TokenCacheRepository repository.Interface
Writer writer.Interface
Logger logger.Interface
Clock clock.Interface
Authentication authentication.Interface
TokenCacheRepository repository.Interface
CredentialPluginReader credentialpluginreader.Interface
CredentialPluginWriter credentialpluginwriter.Interface
Logger logger.Interface
Clock clock.Interface
}

func (u *GetToken) Do(ctx context.Context, in Input) error {
u.Logger.V(1).Infof("WARNING: log may contain your secrets such as token or password")

credentialPluginInput, err := u.CredentialPluginReader.Read()
if err != nil {
return fmt.Errorf("could not read the input of credential plugin: %w", err)
}
u.Logger.V(1).Infof("credential plugin is called with apiVersion: %s", credentialPluginInput.ClientAuthenticationAPIVersion)

u.Logger.V(1).Infof("finding a token from cache directory %s", in.TokenCacheDir)
tokenCacheKey := tokencache.Key{
Provider: in.Provider,
Expand Down Expand Up @@ -88,10 +96,11 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
if !claims.IsExpired(u.Clock) {
u.Logger.V(1).Infof("you already have a valid token until %s", claims.Expiry)
out := credentialplugin.Output{
Token: cachedTokenSet.IDToken,
Expiry: claims.Expiry,
Token: cachedTokenSet.IDToken,
Expiry: claims.Expiry,
ClientAuthenticationAPIVersion: credentialPluginInput.ClientAuthenticationAPIVersion,
}
if err := u.Writer.Write(out); err != nil {
if err := u.CredentialPluginWriter.Write(out); err != nil {
return fmt.Errorf("could not write the token to client-go: %w", err)
}
return nil
Expand Down Expand Up @@ -122,10 +131,11 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
}
u.Logger.V(1).Infof("writing the token to client-go")
out := credentialplugin.Output{
Token: authenticationOutput.TokenSet.IDToken,
Expiry: idTokenClaims.Expiry,
Token: authenticationOutput.TokenSet.IDToken,
Expiry: idTokenClaims.Expiry,
ClientAuthenticationAPIVersion: credentialPluginInput.ClientAuthenticationAPIVersion,
}
if err := u.Writer.Write(out); err != nil {
if err := u.CredentialPluginWriter.Write(out); err != nil {
return fmt.Errorf("could not write the token to client-go: %w", err)
}
return nil
Expand Down
Loading

0 comments on commit 0e9a39a

Please sign in to comment.