Skip to content

Commit

Permalink
Merge pull request #4340 from twz123/admin-kubeconf-gen
Browse files Browse the repository at this point in the history
Honor API port number in kubeconfig admin subcommand
  • Loading branch information
twz123 authored May 3, 2024
2 parents ca40556 + 2ec9c69 commit 252b174
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 8 deletions.
2 changes: 2 additions & 0 deletions cmd/controller/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func (c *Certificates) Init(ctx context.Context) error {
return fmt.Errorf("failed to read ca cert: %w", err)
}
c.CACert = string(cert)
// Changing the URL here also requires changes in the "k0s kubeconfig admin" subcommand.
kubeConfigAPIUrl := fmt.Sprintf("https://localhost:%d", c.ClusterSpec.API.Port)
eg.Go(func() error {
// Front proxy CA
Expand Down Expand Up @@ -257,6 +258,7 @@ func kubeConfig(dest, url, caCert, clientCert, clientKey, owner string) error {

kubeconfig, err := clientcmd.Write(clientcmdapi.Config{
Clusters: map[string]*clientcmdapi.Cluster{clusterName: {
// The server URL is replaced in the "k0s kubeconfig admin" subcommand.
Server: url,
CertificateAuthorityData: []byte(caCert),
}},
Expand Down
42 changes: 34 additions & 8 deletions cmd/kubeconfig/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ limitations under the License.
package kubeconfig

import (
"errors"
"fmt"
"os"
"strings"
"io/fs"

"github.com/k0sproject/k0s/pkg/config"
"github.com/sirupsen/logrus"
"github.com/k0sproject/k0s/pkg/kubernetes"

"k8s.io/client-go/tools/clientcmd"

"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

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

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

// Now replace the internal address with the external one. See
// cmd/controller/certificates.go to see how the original kubeconfig
// is generated.
nodeConfig, err := opts.K0sVars.NodeConfig()
if err != nil {
return err
}
clusterAPIURL := nodeConfig.Spec.API.APIAddressURL()
newContent := strings.Replace(string(content), "https://localhost:6443", clusterAPIURL, -1)
_, err = cmd.OutOrStdout().Write([]byte(newContent))
internalURL := fmt.Sprintf("https://localhost:%d", nodeConfig.Spec.API.Port)
externalURL := nodeConfig.Spec.API.APIAddressURL()
for _, c := range adminConfig.Clusters {
if c.Server == internalURL {
c.Server = externalURL
}
}

data, err := clientcmd.Write(*adminConfig)
if err != nil {
return fmt.Errorf("failed to serialize admin kubeconfig: %w", err)
}

_, err = cmd.OutOrStdout().Write(data)
return err
},
}
Expand Down
122 changes: 122 additions & 0 deletions cmd/kubeconfig/admin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
Copyright 2024 k0s authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package kubeconfig_test

import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/k0sproject/k0s/cmd"
"github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1"
"github.com/k0sproject/k0s/pkg/config"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
"sigs.k8s.io/yaml"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAdmin(t *testing.T) {
dataDir := t.TempDir()

configPath := filepath.Join(dataDir, "k0s.yaml")
writeYAML(t, configPath, &v1beta1.ClusterConfig{
TypeMeta: metav1.TypeMeta{APIVersion: v1beta1.SchemeGroupVersion.String(), Kind: v1beta1.ClusterConfigKind},
Spec: &v1beta1.ClusterSpec{API: &v1beta1.APISpec{
Port: 65432, ExternalAddress: "not-here.example.com",
}},
})

adminConfPath := filepath.Join(dataDir, "admin.conf")
require.NoError(t, clientcmd.WriteToFile(api.Config{
Clusters: map[string]*api.Cluster{
t.Name(): {Server: "https://localhost:65432"},
},
}, adminConfPath))

rtConfigPath := filepath.Join(dataDir, "run", "k0s.yaml")
writeYAML(t, rtConfigPath, &config.RuntimeConfig{
TypeMeta: metav1.TypeMeta{APIVersion: v1beta1.SchemeGroupVersion.String(), Kind: config.RuntimeConfigKind},
Spec: &config.RuntimeConfigSpec{K0sVars: &config.CfgVars{
AdminKubeConfigPath: adminConfPath,
DataDir: dataDir,
RuntimeConfigPath: rtConfigPath,
StartupConfigPath: configPath,
}},
})

var stdout bytes.Buffer
var stderr strings.Builder
underTest := cmd.NewRootCmd()
underTest.SetArgs([]string{"kubeconfig", "--data-dir", dataDir, "admin"})
underTest.SetOut(&stdout)
underTest.SetErr(&stderr)

assert.NoError(t, underTest.Execute())

assert.Empty(t, stderr.String())

adminConf, err := clientcmd.Load(stdout.Bytes())
require.NoError(t, err)

if theCluster, ok := adminConf.Clusters[t.Name()]; assert.True(t, ok) {
assert.Equal(t, "https://not-here.example.com:65432", theCluster.Server)
}
}

func TestAdmin_NoAdminConfig(t *testing.T) {
dataDir := t.TempDir()

configPath := filepath.Join(dataDir, "k0s.yaml")
adminConfPath := filepath.Join(dataDir, "admin.conf")
rtConfigPath := filepath.Join(dataDir, "run", "k0s.yaml")
writeYAML(t, rtConfigPath, &config.RuntimeConfig{
TypeMeta: metav1.TypeMeta{APIVersion: v1beta1.SchemeGroupVersion.String(), Kind: config.RuntimeConfigKind},
Spec: &config.RuntimeConfigSpec{K0sVars: &config.CfgVars{
AdminKubeConfigPath: adminConfPath,
DataDir: dataDir,
RuntimeConfigPath: rtConfigPath,
StartupConfigPath: configPath,
}},
})

var stdout, stderr strings.Builder
underTest := cmd.NewRootCmd()
underTest.SetArgs([]string{"kubeconfig", "--data-dir", dataDir, "admin"})
underTest.SetOut(&stdout)
underTest.SetErr(&stderr)

assert.Error(t, underTest.Execute())

assert.Empty(t, stdout.String())
msg := fmt.Sprintf("admin config %q not found, check if the control plane is initialized on this node", adminConfPath)
assert.Equal(t, "Error: "+msg+"\n", stderr.String())
}

func writeYAML(t *testing.T, path string, data any) {
bytes, err := yaml.Marshal(data)
require.NoError(t, err)
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0755))
require.NoError(t, os.WriteFile(path, bytes, 0644))
}

0 comments on commit 252b174

Please sign in to comment.