Skip to content

Commit 252b174

Browse files
authored
Merge pull request #4340 from twz123/admin-kubeconf-gen
Honor API port number in kubeconfig admin subcommand
2 parents ca40556 + 2ec9c69 commit 252b174

File tree

3 files changed

+158
-8
lines changed

3 files changed

+158
-8
lines changed

cmd/controller/certificates.go

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ func (c *Certificates) Init(ctx context.Context) error {
6363
return fmt.Errorf("failed to read ca cert: %w", err)
6464
}
6565
c.CACert = string(cert)
66+
// Changing the URL here also requires changes in the "k0s kubeconfig admin" subcommand.
6667
kubeConfigAPIUrl := fmt.Sprintf("https://localhost:%d", c.ClusterSpec.API.Port)
6768
eg.Go(func() error {
6869
// Front proxy CA
@@ -257,6 +258,7 @@ func kubeConfig(dest, url, caCert, clientCert, clientKey, owner string) error {
257258

258259
kubeconfig, err := clientcmd.Write(clientcmdapi.Config{
259260
Clusters: map[string]*clientcmdapi.Cluster{clusterName: {
261+
// The server URL is replaced in the "k0s kubeconfig admin" subcommand.
260262
Server: url,
261263
CertificateAuthorityData: []byte(caCert),
262264
}},

cmd/kubeconfig/admin.go

+34-8
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ limitations under the License.
1717
package kubeconfig
1818

1919
import (
20+
"errors"
2021
"fmt"
21-
"os"
22-
"strings"
22+
"io/fs"
2323

2424
"github.com/k0sproject/k0s/pkg/config"
25-
"github.com/sirupsen/logrus"
25+
"github.com/k0sproject/k0s/pkg/kubernetes"
26+
27+
"k8s.io/client-go/tools/clientcmd"
2628

29+
"github.com/sirupsen/logrus"
2730
"github.com/spf13/cobra"
2831
)
2932

@@ -45,18 +48,41 @@ func kubeConfigAdminCmd() *cobra.Command {
4548
return err
4649
}
4750

48-
content, err := os.ReadFile(opts.K0sVars.AdminKubeConfigPath)
51+
// The admin kubeconfig in k0s' data dir uses the internal cluster
52+
// address. This command is intended to provide a kubeconfig that
53+
// uses the external address. Load the existing admin kubeconfig and
54+
// rewrite it.
55+
adminConfig, err := kubernetes.KubeconfigFromFile(opts.K0sVars.AdminKubeConfigPath)()
56+
if pathErr := (*fs.PathError)(nil); errors.As(err, &pathErr) &&
57+
pathErr.Path == opts.K0sVars.AdminKubeConfigPath &&
58+
errors.Is(pathErr.Err, fs.ErrNotExist) {
59+
return fmt.Errorf("admin config %q not found, check if the control plane is initialized on this node", pathErr.Path)
60+
}
4961
if err != nil {
50-
return fmt.Errorf("failed to read admin config, check if the control plane is initialized on this node: %w", err)
62+
return fmt.Errorf("failed to load admin config: %w", err)
5163
}
5264

65+
// Now replace the internal address with the external one. See
66+
// cmd/controller/certificates.go to see how the original kubeconfig
67+
// is generated.
5368
nodeConfig, err := opts.K0sVars.NodeConfig()
5469
if err != nil {
5570
return err
5671
}
57-
clusterAPIURL := nodeConfig.Spec.API.APIAddressURL()
58-
newContent := strings.Replace(string(content), "https://localhost:6443", clusterAPIURL, -1)
59-
_, err = cmd.OutOrStdout().Write([]byte(newContent))
72+
internalURL := fmt.Sprintf("https://localhost:%d", nodeConfig.Spec.API.Port)
73+
externalURL := nodeConfig.Spec.API.APIAddressURL()
74+
for _, c := range adminConfig.Clusters {
75+
if c.Server == internalURL {
76+
c.Server = externalURL
77+
}
78+
}
79+
80+
data, err := clientcmd.Write(*adminConfig)
81+
if err != nil {
82+
return fmt.Errorf("failed to serialize admin kubeconfig: %w", err)
83+
}
84+
85+
_, err = cmd.OutOrStdout().Write(data)
6086
return err
6187
},
6288
}

cmd/kubeconfig/admin_test.go

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
Copyright 2024 k0s authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package kubeconfig_test
18+
19+
import (
20+
"bytes"
21+
"fmt"
22+
"os"
23+
"path/filepath"
24+
"strings"
25+
"testing"
26+
27+
"github.com/k0sproject/k0s/cmd"
28+
"github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1"
29+
"github.com/k0sproject/k0s/pkg/config"
30+
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/client-go/tools/clientcmd"
33+
"k8s.io/client-go/tools/clientcmd/api"
34+
"sigs.k8s.io/yaml"
35+
36+
"github.com/stretchr/testify/assert"
37+
"github.com/stretchr/testify/require"
38+
)
39+
40+
func TestAdmin(t *testing.T) {
41+
dataDir := t.TempDir()
42+
43+
configPath := filepath.Join(dataDir, "k0s.yaml")
44+
writeYAML(t, configPath, &v1beta1.ClusterConfig{
45+
TypeMeta: metav1.TypeMeta{APIVersion: v1beta1.SchemeGroupVersion.String(), Kind: v1beta1.ClusterConfigKind},
46+
Spec: &v1beta1.ClusterSpec{API: &v1beta1.APISpec{
47+
Port: 65432, ExternalAddress: "not-here.example.com",
48+
}},
49+
})
50+
51+
adminConfPath := filepath.Join(dataDir, "admin.conf")
52+
require.NoError(t, clientcmd.WriteToFile(api.Config{
53+
Clusters: map[string]*api.Cluster{
54+
t.Name(): {Server: "https://localhost:65432"},
55+
},
56+
}, adminConfPath))
57+
58+
rtConfigPath := filepath.Join(dataDir, "run", "k0s.yaml")
59+
writeYAML(t, rtConfigPath, &config.RuntimeConfig{
60+
TypeMeta: metav1.TypeMeta{APIVersion: v1beta1.SchemeGroupVersion.String(), Kind: config.RuntimeConfigKind},
61+
Spec: &config.RuntimeConfigSpec{K0sVars: &config.CfgVars{
62+
AdminKubeConfigPath: adminConfPath,
63+
DataDir: dataDir,
64+
RuntimeConfigPath: rtConfigPath,
65+
StartupConfigPath: configPath,
66+
}},
67+
})
68+
69+
var stdout bytes.Buffer
70+
var stderr strings.Builder
71+
underTest := cmd.NewRootCmd()
72+
underTest.SetArgs([]string{"kubeconfig", "--data-dir", dataDir, "admin"})
73+
underTest.SetOut(&stdout)
74+
underTest.SetErr(&stderr)
75+
76+
assert.NoError(t, underTest.Execute())
77+
78+
assert.Empty(t, stderr.String())
79+
80+
adminConf, err := clientcmd.Load(stdout.Bytes())
81+
require.NoError(t, err)
82+
83+
if theCluster, ok := adminConf.Clusters[t.Name()]; assert.True(t, ok) {
84+
assert.Equal(t, "https://not-here.example.com:65432", theCluster.Server)
85+
}
86+
}
87+
88+
func TestAdmin_NoAdminConfig(t *testing.T) {
89+
dataDir := t.TempDir()
90+
91+
configPath := filepath.Join(dataDir, "k0s.yaml")
92+
adminConfPath := filepath.Join(dataDir, "admin.conf")
93+
rtConfigPath := filepath.Join(dataDir, "run", "k0s.yaml")
94+
writeYAML(t, rtConfigPath, &config.RuntimeConfig{
95+
TypeMeta: metav1.TypeMeta{APIVersion: v1beta1.SchemeGroupVersion.String(), Kind: config.RuntimeConfigKind},
96+
Spec: &config.RuntimeConfigSpec{K0sVars: &config.CfgVars{
97+
AdminKubeConfigPath: adminConfPath,
98+
DataDir: dataDir,
99+
RuntimeConfigPath: rtConfigPath,
100+
StartupConfigPath: configPath,
101+
}},
102+
})
103+
104+
var stdout, stderr strings.Builder
105+
underTest := cmd.NewRootCmd()
106+
underTest.SetArgs([]string{"kubeconfig", "--data-dir", dataDir, "admin"})
107+
underTest.SetOut(&stdout)
108+
underTest.SetErr(&stderr)
109+
110+
assert.Error(t, underTest.Execute())
111+
112+
assert.Empty(t, stdout.String())
113+
msg := fmt.Sprintf("admin config %q not found, check if the control plane is initialized on this node", adminConfPath)
114+
assert.Equal(t, "Error: "+msg+"\n", stderr.String())
115+
}
116+
117+
func writeYAML(t *testing.T, path string, data any) {
118+
bytes, err := yaml.Marshal(data)
119+
require.NoError(t, err)
120+
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0755))
121+
require.NoError(t, os.WriteFile(path, bytes, 0644))
122+
}

0 commit comments

Comments
 (0)