Skip to content

Commit cd213e7

Browse files
committed
Allow retrieving kubeconfigs without exec config, too.
1 parent af085ac commit cd213e7

File tree

5 files changed

+103
-61
lines changed

5 files changed

+103
-61
lines changed

cmd/admin/v1/cluster.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,15 @@ func newClusterCmd(c *config.Config) *cobra.Command {
7272
}
7373

7474
kubeconfigCmd.Flags().DurationP("expiration", "", 8*time.Hour, "kubeconfig will expire after given time")
75-
kubeconfigCmd.Flags().Bool("merge", true, "merges the kubeconfig into default kubeconfig instead of printing it to the console")
75+
kubeconfigCmd.Flags().Bool("merge", true, "merges the kubeconfig into the current kubeconfig")
76+
kubeconfigCmd.Flags().Bool("print-only", false, "only prints the kubeconfig to the console instead of writing it")
77+
kubeconfigCmd.Flags().String("auth-type", string(kubernetes.AuthTypeExec), `the way how the resulting kubeconfig authenticates at the api server. can be "exec" or "certs".
78+
"exec" injects an exec config into the kubeconfig, which uses this CLI to automatically renew certificates when they expire.
79+
"certs" simply adds the client certificates to the kubeconfig, there is no automatic renewal once the certificates have expired, the CLI is not called automatically.`)
7680
kubeconfigCmd.Flags().String("kubeconfig", "", "specify an explicit path for the merged kubeconfig to be written, defaults to default kubeconfig paths if not provided")
7781

82+
genericcli.Must(kubeconfigCmd.RegisterFlagCompletionFunc("auth-type", c.Completion.ClusterKubeconfigAuthType))
83+
7884
// metal admin cluster machine list
7985

8086
machineListCmd := &cobra.Command{
@@ -195,26 +201,22 @@ func (c *cluster) kubeconfig(args []string) error {
195201
return fmt.Errorf("failed to get cluster credentials: %w", err)
196202
}
197203

198-
if !viper.GetBool("merge") {
199-
_, _ = fmt.Fprintln(c.c.Out, resp.Msg.Kubeconfig)
200-
return nil
201-
}
202-
203-
var (
204-
kubeconfigPath = viper.GetString("kubeconfig")
205-
)
206-
207-
merged, err := kubernetes.MergeKubeconfig(c.c.Fs, []byte(resp.Msg.Kubeconfig), pointer.PointerOrNil(kubeconfigPath), nil, c.c.GetProject(), id) // FIXME: reverse lookup project name
204+
kubeconfig, err := kubernetes.NewKubeconfigFromRaw(c.c.Fs, []byte(resp.Msg.Kubeconfig), nil, c.c.GetProject(), id) // FIXME: reverse lookup project name
208205
if err != nil {
209206
return err
210207
}
211208

212-
err = afero.WriteFile(c.c.Fs, merged.Path, merged.Raw, 0600)
209+
if viper.GetBool("print-only") {
210+
_, _ = fmt.Fprintln(c.c.Out, string(kubeconfig.Raw))
211+
return nil
212+
}
213+
214+
err = afero.WriteFile(c.c.Fs, kubeconfig.Path, kubeconfig.Raw, 0600)
213215
if err != nil {
214216
return fmt.Errorf("unable to write merged kubeconfig: %w", err)
215217
}
216218

217-
_, _ = fmt.Fprintf(c.c.Out, "%s merged context %q into %s\n", color.GreenString("✔"), merged.ContextName, merged.Path)
219+
_, _ = fmt.Fprintf(c.c.Out, "%s merged context %q into %s\n", color.GreenString("✔"), kubeconfig.ContextName, kubeconfig.Path)
218220

219221
return nil
220222
}

cmd/api/v1/cluster.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,15 @@ func newClusterCmd(c *config.Config) *cobra.Command {
116116

117117
kubeconfigCmd.Flags().StringP("project", "p", "", "the project in which the cluster resides for which to get the kubeconfig for")
118118
kubeconfigCmd.Flags().DurationP("expiration", "", 8*time.Hour, "kubeconfig will expire after given time")
119-
kubeconfigCmd.Flags().Bool("merge", true, "merges the kubeconfig into default kubeconfig instead of printing it to the console")
119+
kubeconfigCmd.Flags().Bool("merge", true, "merges the kubeconfig into the current kubeconfig")
120+
kubeconfigCmd.Flags().Bool("print-only", false, "only prints the kubeconfig to the console instead of writing it")
121+
kubeconfigCmd.Flags().String("auth-type", string(kubernetes.AuthTypeExec), `the way how the resulting kubeconfig authenticates at the api server. can be "exec" or "certs".
122+
"exec" injects an exec config into the kubeconfig, which uses this CLI to automatically renew certificates when they expire.
123+
"certs" simply adds the client certificates to the kubeconfig, there is no automatic renewal once the certificates have expired, the CLI is not called automatically.`)
120124
kubeconfigCmd.Flags().String("kubeconfig", "", "specify an explicit path for the merged kubeconfig to be written, defaults to default kubeconfig paths if not provided")
121125

122126
genericcli.Must(kubeconfigCmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion))
127+
genericcli.Must(kubeconfigCmd.RegisterFlagCompletionFunc("auth-type", c.Completion.ClusterKubeconfigAuthType))
123128

124129
execConfigCmd := &cobra.Command{
125130
Use: "exec-config",
@@ -563,32 +568,31 @@ func (c *cluster) kubeconfig(args []string) error {
563568
return fmt.Errorf("failed to get cluster credentials: %w", err)
564569
}
565570

566-
if !viper.GetBool("merge") {
567-
_, _ = fmt.Fprintln(c.c.Out, resp.Msg.Kubeconfig)
568-
return nil
569-
}
570-
571571
projectResp, err := c.c.Client.Apiv1().Project().Get(ctx, connect.NewRequest(&apiv1.ProjectServiceGetRequest{Project: c.c.GetProject()}))
572572
if err != nil {
573573
return err
574574
}
575575

576576
var (
577-
kubeconfigPath = viper.GetString("kubeconfig")
578-
projectName = helpers.TrimProvider(projectResp.Msg.Project.Name)
577+
projectName = helpers.TrimProvider(projectResp.Msg.Project.Name)
579578
)
580579

581-
merged, err := kubernetes.MergeKubeconfig(c.c.Fs, []byte(resp.Msg.Kubeconfig), pointer.PointerOrNil(kubeconfigPath), &projectName, projectResp.Msg.Project.Uuid, id)
580+
kubeconfig, err := kubernetes.NewKubeconfigFromRaw(c.c.Fs, []byte(resp.Msg.Kubeconfig), &projectName, projectResp.Msg.Project.Uuid, id)
582581
if err != nil {
583582
return err
584583
}
585584

586-
err = afero.WriteFile(c.c.Fs, merged.Path, merged.Raw, 0600)
585+
if viper.GetBool("print-only") {
586+
_, _ = fmt.Fprintln(c.c.Out, string(kubeconfig.Raw))
587+
return nil
588+
}
589+
590+
err = afero.WriteFile(c.c.Fs, kubeconfig.Path, kubeconfig.Raw, 0600)
587591
if err != nil {
588592
return fmt.Errorf("unable to write merged kubeconfig: %w", err)
589593
}
590594

591-
_, _ = fmt.Fprintf(c.c.Out, "%s merged context %q into %s\n", color.GreenString("✔"), merged.ContextName, merged.Path)
595+
_, _ = fmt.Fprintf(c.c.Out, "%s merged context %q into %s\n", color.GreenString("✔"), kubeconfig.ContextName, kubeconfig.Path)
592596

593597
return nil
594598
}

cmd/completion/cluster.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"connectrpc.com/connect"
55
adminv1 "github.com/metal-stack-cloud/api/go/admin/v1"
66
apiv1 "github.com/metal-stack-cloud/api/go/api/v1"
7+
"github.com/metal-stack-cloud/cli/cmd/kubernetes"
78
"github.com/metal-stack/metal-lib/pkg/genericcli"
89
"github.com/spf13/cobra"
910
)
@@ -107,8 +108,6 @@ func (c *Completion) AdminClusterFirewallListCompletion(cmd *cobra.Command, args
107108

108109
var names []string
109110
for _, machine := range resp.Msg.Machines {
110-
machine := machine
111-
112111
if machine.Role != "firewall" {
113112
continue
114113
}
@@ -118,3 +117,7 @@ func (c *Completion) AdminClusterFirewallListCompletion(cmd *cobra.Command, args
118117

119118
return names, cobra.ShellCompDirectiveNoFileComp
120119
}
120+
121+
func (c *Completion) ClusterKubeconfigAuthType(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
122+
return []string{string(kubernetes.AuthTypeExec), string(kubernetes.AuthTypeClientCerts)}, cobra.ShellCompDirectiveNoFileComp
123+
}
Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/spf13/afero"
9+
"github.com/spf13/viper"
910
"k8s.io/apimachinery/pkg/runtime"
1011
"k8s.io/client-go/tools/clientcmd"
1112
"k8s.io/client-go/tools/clientcmd/api"
@@ -14,16 +15,23 @@ import (
1415
configv1 "k8s.io/client-go/tools/clientcmd/api/v1"
1516
)
1617

17-
type MergedKubeconfig struct {
18+
type authType string
19+
20+
const (
21+
AuthTypeClientCerts authType = "certs"
22+
AuthTypeExec authType = "exec"
23+
)
24+
25+
type Kubeconfig struct {
1826
Raw []byte
1927
Path string
2028
ContextName string
2129
}
2230

23-
func MergeKubeconfig(fs afero.Fs, raw []byte, kubeconfigPath, projectName *string, projectid, clusterid string) (*MergedKubeconfig, error) {
31+
func NewKubeconfigFromRaw(fs afero.Fs, raw []byte, projectName *string, projectid, clusterid string) (*Kubeconfig, error) {
2432
path := os.Getenv(clientcmd.RecommendedConfigPathEnvVar)
25-
if kubeconfigPath != nil {
26-
path = *kubeconfigPath
33+
if userPath := viper.GetString("kubeconfig"); userPath != "" {
34+
path = userPath
2735
}
2836
if path == "" {
2937
path = clientcmd.RecommendedHomeFile
@@ -40,13 +48,8 @@ func MergeKubeconfig(fs afero.Fs, raw []byte, kubeconfigPath, projectName *strin
4048
}
4149
}
4250

43-
currentConfig, err := clientcmd.LoadFromFile(path)
44-
if err != nil {
45-
return nil, fmt.Errorf("error loading kubeconfig: %w", err)
46-
}
47-
4851
kubeconfig := &configv1.Config{}
49-
err = runtime.DecodeInto(configlatest.Codec, raw, kubeconfig)
52+
err := runtime.DecodeInto(configlatest.Codec, raw, kubeconfig)
5053
if err != nil {
5154
return nil, fmt.Errorf("unable to decode kubeconfig: %w", err)
5255
}
@@ -58,8 +61,6 @@ func MergeKubeconfig(fs afero.Fs, raw []byte, kubeconfigPath, projectName *strin
5861
)
5962

6063
for _, cl := range kubeconfig.Clusters {
61-
cl := cl
62-
6364
prefix, _, found := strings.Cut(cl.Name, "-external")
6465
if !found {
6566
continue
@@ -73,8 +74,6 @@ func MergeKubeconfig(fs afero.Fs, raw []byte, kubeconfigPath, projectName *strin
7374
}
7475

7576
for _, a := range kubeconfig.AuthInfos {
76-
a := a
77-
7877
if !strings.HasSuffix(a.Name, clusterName+"-external") {
7978
continue
8079
}
@@ -91,45 +90,75 @@ func MergeKubeconfig(fs afero.Fs, raw []byte, kubeconfigPath, projectName *strin
9190
contextName = fmt.Sprintf("%s-%s@metalstack.cloud", clusterName, *projectName)
9291
}
9392

93+
currentConfig := &api.Config{
94+
Clusters: map[string]*api.Cluster{},
95+
Contexts: map[string]*api.Context{},
96+
AuthInfos: map[string]*api.AuthInfo{},
97+
}
98+
if viper.GetBool("merge") {
99+
var err error
100+
currentConfig, err = clientcmd.LoadFromFile(path)
101+
if err != nil {
102+
return nil, fmt.Errorf("error loading kubeconfig: %w", err)
103+
}
104+
}
105+
94106
currentConfig.Contexts[contextName] = &api.Context{
95107
Cluster: contextName,
96108
AuthInfo: contextName,
97109
}
110+
98111
currentConfig.Clusters[contextName] = &api.Cluster{
99112
Server: cluster.Cluster.Server,
100113
CertificateAuthorityData: cluster.Cluster.CertificateAuthorityData,
101114
}
102115

103-
metalcli, err := os.Executable()
104-
if err != nil {
105-
return nil, fmt.Errorf("unable to get executable path: %w", err)
116+
if currentConfig.CurrentContext == "" {
117+
currentConfig.CurrentContext = contextName
106118
}
107-
currentConfig.AuthInfos[contextName] = &api.AuthInfo{
108-
Exec: &api.ExecConfig{
109-
Command: metalcli,
110-
Args: []string{"cluster", "exec-config", "-p", projectid, clusterid},
111-
APIVersion: "client.authentication.k8s.io/v1", // since k8s 1.22, if earlier versions are used, the API version is client.authentication.k8s.io/v1beta1
112-
InteractiveMode: api.IfAvailableExecInteractiveMode,
113-
},
119+
120+
auth := AuthTypeExec
121+
if viper.GetString("auth-type") != "" {
122+
auth = authType(viper.GetString("auth-type"))
114123
}
115124

116-
if currentConfig.CurrentContext == "" {
117-
currentConfig.CurrentContext = contextName
125+
switch auth {
126+
case AuthTypeExec:
127+
metalcli, err := os.Executable()
128+
if err != nil {
129+
return nil, fmt.Errorf("unable to get executable path: %w", err)
130+
}
131+
132+
currentConfig.AuthInfos[contextName] = &api.AuthInfo{
133+
Exec: &api.ExecConfig{
134+
Command: metalcli,
135+
Args: []string{"cluster", "exec-config", "-p", projectid, clusterid},
136+
APIVersion: "client.authentication.k8s.io/v1", // since k8s 1.22, if earlier versions are used, the API version is client.authentication.k8s.io/v1beta1
137+
InteractiveMode: api.IfAvailableExecInteractiveMode,
138+
},
139+
}
140+
141+
ec, err := NewUserExecCache(fs)
142+
if err != nil {
143+
return nil, err
144+
}
145+
// remove cached credentials so a new one will be created
146+
_ = ec.Clean(clusterid)
147+
case AuthTypeClientCerts:
148+
currentConfig.AuthInfos[contextName] = &api.AuthInfo{
149+
ClientCertificateData: authInfo.ClientCertificateData,
150+
ClientKeyData: authInfo.ClientKeyData,
151+
}
152+
default:
153+
return nil, fmt.Errorf("unsupported auth type for kubeconfig: %s", auth)
118154
}
119155

120156
merged, err := runtime.Encode(configlatest.Codec, currentConfig)
121157
if err != nil {
122158
return nil, fmt.Errorf("unable to encode kubeconfig: %w", err)
123159
}
124160

125-
ec, err := NewUserExecCache(fs)
126-
if err != nil {
127-
return nil, err
128-
}
129-
// remove cached credentials so a new one will be created
130-
_ = ec.Clean(clusterid)
131-
132-
return &MergedKubeconfig{
161+
return &Kubeconfig{
133162
Raw: merged,
134163
ContextName: contextName,
135164
Path: path,

docs/metal_cluster_kubeconfig.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@ metal cluster kubeconfig [flags]
99
### Options
1010

1111
```
12+
--auth-type string the way how the resulting kubeconfig authenticates at the api server. can be "exec" or "certs".
13+
"exec" injects an exec config into the kubeconfig, which uses this CLI to automatically renew certificates when they expire.
14+
"certs" simply adds the client certificates to the kubeconfig, there is no automatic renewal once the certificates have expired, the CLI is not called automatically. (default "exec")
1215
--expiration duration kubeconfig will expire after given time (default 8h0m0s)
1316
-h, --help help for kubeconfig
1417
--kubeconfig string specify an explicit path for the merged kubeconfig to be written, defaults to default kubeconfig paths if not provided
15-
--merge merges the kubeconfig into default kubeconfig instead of printing it to the console (default true)
18+
--merge merges the kubeconfig into the current kubeconfig (default true)
19+
--print-only only prints the kubeconfig to the console instead of writing it
1620
-p, --project string the project in which the cluster resides for which to get the kubeconfig for
1721
```
1822

0 commit comments

Comments
 (0)