mirror of https://github.com/k3d-io/k3d
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
13 KiB
362 lines
13 KiB
/*
|
|
Copyright © 2020-2023 The k3d Author(s)
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
*/
|
|
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
l "github.com/k3d-io/k3d/v5/pkg/logger"
|
|
"github.com/k3d-io/k3d/v5/pkg/runtimes"
|
|
k3d "github.com/k3d-io/k3d/v5/pkg/types"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
|
)
|
|
|
|
// WriteKubeConfigOptions provide a set of options for writing a KubeConfig file
|
|
type WriteKubeConfigOptions struct {
|
|
UpdateExisting bool
|
|
UpdateCurrentContext bool
|
|
OverwriteExisting bool
|
|
}
|
|
|
|
// KubeconfigGetWrite ...
|
|
// 1. fetches the KubeConfig from the first server node retrieved for a given cluster
|
|
// 2. modifies it by updating some fields with cluster-specific information
|
|
// 3. writes it to the specified output
|
|
func KubeconfigGetWrite(ctx context.Context, runtime runtimes.Runtime, cluster *k3d.Cluster, output string, writeKubeConfigOptions *WriteKubeConfigOptions) (string, error) {
|
|
// get kubeconfig from cluster node
|
|
kubeconfig, err := KubeconfigGet(ctx, runtime, cluster)
|
|
if err != nil {
|
|
return output, fmt.Errorf("failed to get kubeconfig for cluster '%s': %w", cluster.Name, err)
|
|
}
|
|
|
|
// empty output parameter = write to default
|
|
if output == "" {
|
|
output, err = KubeconfigGetDefaultPath()
|
|
if err != nil {
|
|
return output, fmt.Errorf("failed to get default kubeconfig path: %w", err)
|
|
}
|
|
}
|
|
|
|
// simply write to the output, ignoring existing contents
|
|
if writeKubeConfigOptions.OverwriteExisting || output == "-" {
|
|
return output, KubeconfigWriteToPath(ctx, kubeconfig, output)
|
|
}
|
|
|
|
// load config from existing file or fail if it has non-kubeconfig contents
|
|
var existingKubeConfig *clientcmdapi.Config
|
|
firstRun := true
|
|
for {
|
|
existingKubeConfig, err = clientcmd.LoadFromFile(output) // will return an empty config if file is empty
|
|
if err != nil {
|
|
// the output file does not exist: try to create it and try again
|
|
if os.IsNotExist(err) && firstRun {
|
|
l.Log().Debugf("Output path '%s' doesn't exist, trying to create it...", output)
|
|
|
|
// create directory path
|
|
if err := os.MkdirAll(filepath.Dir(output), 0755); err != nil {
|
|
return output, fmt.Errorf("failed to create output directory '%s': %w", filepath.Dir(output), err)
|
|
}
|
|
|
|
// try create output file
|
|
f, err := os.Create(output)
|
|
if err != nil {
|
|
return output, fmt.Errorf("failed to create output file '%s': %w", output, err)
|
|
}
|
|
f.Close()
|
|
|
|
// try again, but do not try to create the file this time
|
|
firstRun = false
|
|
continue
|
|
}
|
|
return output, fmt.Errorf("failed to open output file '%s' or it's not a kubeconfig: %w", output, err)
|
|
}
|
|
break
|
|
}
|
|
|
|
// update existing kubeconfig, but error out if there are conflicting fields but we don't want to update them
|
|
return output, KubeconfigMerge(ctx, kubeconfig, existingKubeConfig, output, writeKubeConfigOptions.UpdateExisting, writeKubeConfigOptions.UpdateCurrentContext)
|
|
}
|
|
|
|
// KubeconfigGet grabs the kubeconfig file from /output from a server node container,
|
|
// modifies it by updating some fields with cluster-specific information
|
|
// and returns a Config object for further processing
|
|
func KubeconfigGet(ctx context.Context, runtime runtimes.Runtime, cluster *k3d.Cluster) (*clientcmdapi.Config, error) {
|
|
// get all server nodes for the selected cluster
|
|
// TODO: getKubeconfig: we should make sure, that the server node we're trying to fetch from is actually running
|
|
serverNodes, err := runtime.GetNodesByLabel(ctx, map[string]string{k3d.LabelClusterName: cluster.Name, k3d.LabelRole: string(k3d.ServerRole)})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("runtime failed to get server nodes for cluster '%s': %w", cluster.Name, err)
|
|
}
|
|
if len(serverNodes) == 0 {
|
|
return nil, fmt.Errorf("didn't find any server node for cluster '%s'", cluster.Name)
|
|
}
|
|
|
|
// prefer a server node, which actually has the port exposed
|
|
var chosenServer *k3d.Node
|
|
chosenServer = nil
|
|
APIPort := k3d.DefaultAPIPort
|
|
APIHost := k3d.DefaultAPIHost
|
|
|
|
for _, server := range serverNodes {
|
|
if _, ok := server.RuntimeLabels[k3d.LabelServerAPIPort]; ok {
|
|
chosenServer = server
|
|
APIPort = server.RuntimeLabels[k3d.LabelServerAPIPort]
|
|
if _, ok := server.RuntimeLabels[k3d.LabelServerAPIHost]; ok {
|
|
APIHost = server.RuntimeLabels[k3d.LabelServerAPIHost]
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if chosenServer == nil {
|
|
chosenServer = serverNodes[0]
|
|
}
|
|
// get the kubeconfig from the first server node
|
|
reader, err := runtime.GetKubeconfig(ctx, chosenServer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("runtime failed to pull kubeconfig from node '%s': %w", chosenServer.Name, err)
|
|
}
|
|
defer reader.Close()
|
|
|
|
readBytes, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read kubeconfig file: %w", err)
|
|
}
|
|
|
|
// drop the first 512 bytes which contain file metadata/control characters
|
|
// and trim any NULL characters
|
|
trimBytes := bytes.Trim(readBytes[512:], "\x00")
|
|
|
|
/*
|
|
* Modify the kubeconfig
|
|
*/
|
|
kc, err := clientcmd.Load(trimBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse kubeconfig: %w", err)
|
|
}
|
|
|
|
// update the server URL
|
|
kc.Clusters["default"].Server = fmt.Sprintf("https://%s:%s", APIHost, APIPort)
|
|
|
|
// rename user from default to admin
|
|
newAuthInfoName := fmt.Sprintf("admin@%s-%s", k3d.DefaultObjectNamePrefix, cluster.Name)
|
|
kc.AuthInfos[newAuthInfoName] = kc.AuthInfos["default"]
|
|
delete(kc.AuthInfos, "default")
|
|
|
|
// rename cluster from default to clustername
|
|
newClusterName := fmt.Sprintf("%s-%s", k3d.DefaultObjectNamePrefix, cluster.Name)
|
|
kc.Clusters[newClusterName] = kc.Clusters["default"]
|
|
delete(kc.Clusters, "default")
|
|
|
|
// rename context from default to clustername
|
|
newContextName := fmt.Sprintf("%s-%s", k3d.DefaultObjectNamePrefix, cluster.Name)
|
|
kc.Contexts[newContextName] = kc.Contexts["default"]
|
|
delete(kc.Contexts, "default")
|
|
|
|
// update context with new values for cluster and user
|
|
kc.Contexts[newContextName].AuthInfo = newAuthInfoName
|
|
kc.Contexts[newContextName].Cluster = newClusterName
|
|
|
|
// set current-context to new context name
|
|
kc.CurrentContext = newContextName
|
|
|
|
l.Log().Tracef("Modified Kubeconfig: %+v", kc)
|
|
|
|
return kc, nil
|
|
}
|
|
|
|
// KubeconfigWriteToPath takes a kubeconfig and writes it to some path, which can be '-' for os.Stdout
|
|
func KubeconfigWriteToPath(ctx context.Context, kubeconfig *clientcmdapi.Config, path string) error {
|
|
var output *os.File
|
|
defer output.Close()
|
|
var err error
|
|
|
|
if path == "-" {
|
|
output = os.Stdout
|
|
} else {
|
|
output, err = os.Create(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create file '%s': %w", path, err)
|
|
}
|
|
defer output.Close()
|
|
}
|
|
|
|
err = KubeconfigWriteToStream(ctx, kubeconfig, output)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write file '%s': %w", output.Name(), err)
|
|
}
|
|
|
|
l.Log().Debugf("Wrote kubeconfig to '%s'", output.Name())
|
|
|
|
return nil
|
|
}
|
|
|
|
// KubeconfigWriteToStream takes a kubeconfig and writes it to stream
|
|
func KubeconfigWriteToStream(ctx context.Context, kubeconfig *clientcmdapi.Config, writer io.Writer) error {
|
|
kubeconfigBytes, err := clientcmd.Write(*kubeconfig)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write kubeconfig: %w", err)
|
|
}
|
|
|
|
_, err = writer.Write(kubeconfigBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write stream '%s'", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// KubeconfigMerge merges a new kubeconfig into an existing kubeconfig and returns the result
|
|
func KubeconfigMerge(ctx context.Context, newKubeConfig *clientcmdapi.Config, existingKubeConfig *clientcmdapi.Config, outPath string, overwriteConflicting bool, updateCurrentContext bool) error {
|
|
l.Log().Tracef("Merging new Kubeconfig:\n%+v\n>>> into existing Kubeconfig:\n%+v", newKubeConfig, existingKubeConfig)
|
|
|
|
// Overwrite values in existing kubeconfig
|
|
for k, v := range newKubeConfig.Clusters {
|
|
if _, ok := existingKubeConfig.Clusters[k]; ok {
|
|
if !overwriteConflicting {
|
|
return fmt.Errorf("cluster '%s' already exists in target KubeConfig", k)
|
|
}
|
|
}
|
|
existingKubeConfig.Clusters[k] = v
|
|
}
|
|
|
|
for k, v := range newKubeConfig.AuthInfos {
|
|
if _, ok := existingKubeConfig.AuthInfos[k]; ok {
|
|
if !overwriteConflicting {
|
|
return fmt.Errorf("AuthInfo '%s' already exists in target KubeConfig", k)
|
|
}
|
|
}
|
|
existingKubeConfig.AuthInfos[k] = v
|
|
}
|
|
|
|
for k, v := range newKubeConfig.Contexts {
|
|
if _, ok := existingKubeConfig.Contexts[k]; ok && !overwriteConflicting {
|
|
return fmt.Errorf("context '%s' already exists in target KubeConfig", k)
|
|
}
|
|
existingKubeConfig.Contexts[k] = v
|
|
}
|
|
|
|
// Set current context if it's
|
|
// a) empty
|
|
// b) not empty, but we want to update it
|
|
if existingKubeConfig.CurrentContext == "" {
|
|
updateCurrentContext = true
|
|
}
|
|
if updateCurrentContext {
|
|
l.Log().Debugf("Setting new current-context '%s'", newKubeConfig.CurrentContext)
|
|
existingKubeConfig.CurrentContext = newKubeConfig.CurrentContext
|
|
}
|
|
|
|
return KubeconfigWrite(ctx, existingKubeConfig, outPath)
|
|
}
|
|
|
|
// KubeconfigWrite writes a kubeconfig to a path atomically
|
|
func KubeconfigWrite(ctx context.Context, kubeconfig *clientcmdapi.Config, path string) error {
|
|
tempPath := fmt.Sprintf("%s.k3d_%s", path, time.Now().Format("20060102_150405.000000"))
|
|
if err := clientcmd.WriteToFile(*kubeconfig, tempPath); err != nil {
|
|
return fmt.Errorf("failed to write merged kubeconfig to temporary file '%s': %w", tempPath, err)
|
|
}
|
|
|
|
// In case path is a symlink, retrives the name of the target
|
|
realPath, err := filepath.EvalSymlinks(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to follow symlink '%s': %w", path, err)
|
|
}
|
|
|
|
// Move temporary file over existing KubeConfig
|
|
if err := os.Rename(tempPath, realPath); err != nil {
|
|
return fmt.Errorf("failed to overwrite existing KubeConfig '%s' with new kubeconfig '%s': %w", path, tempPath, err)
|
|
}
|
|
|
|
extraLog := ""
|
|
if filepath.Clean(path) != realPath {
|
|
extraLog = fmt.Sprintf("(via symlink '%s')", path)
|
|
}
|
|
l.Log().Debugf("Wrote kubeconfig to '%s' %s", realPath, extraLog)
|
|
|
|
return nil
|
|
}
|
|
|
|
// KubeconfigGetDefaultFile loads the default KubeConfig file
|
|
func KubeconfigGetDefaultFile() (*clientcmdapi.Config, error) {
|
|
path, err := KubeconfigGetDefaultPath()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get default kubeconfig path: %w", err)
|
|
}
|
|
l.Log().Debugf("Using default kubeconfig '%s'", path)
|
|
return clientcmd.LoadFromFile(path)
|
|
}
|
|
|
|
// KubeconfigGetDefaultPath returns the path of the default kubeconfig, but errors if the KUBECONFIG env var specifies more than one file
|
|
func KubeconfigGetDefaultPath() (string, error) {
|
|
defaultKubeConfigLoadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
|
if len(defaultKubeConfigLoadingRules.GetLoadingPrecedence()) > 1 {
|
|
return "", fmt.Errorf("multiple kubeconfigs specified via KUBECONFIG env var: Please reduce to one entry, unset KUBECONFIG or explicitly choose an output")
|
|
}
|
|
return defaultKubeConfigLoadingRules.GetDefaultFilename(), nil
|
|
}
|
|
|
|
// KubeconfigRemoveClusterFromDefaultConfig removes a cluster's details from the default kubeconfig
|
|
func KubeconfigRemoveClusterFromDefaultConfig(ctx context.Context, cluster *k3d.Cluster) error {
|
|
defaultKubeConfigPath, err := KubeconfigGetDefaultPath()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get default kubeconfig path: %w", err)
|
|
}
|
|
kubeconfig, err := KubeconfigGetDefaultFile()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get default kubeconfig file: %w", err)
|
|
}
|
|
kubeconfig = KubeconfigRemoveCluster(ctx, cluster, kubeconfig)
|
|
return KubeconfigWrite(ctx, kubeconfig, defaultKubeConfigPath)
|
|
}
|
|
|
|
// KubeconfigRemoveCluster removes a cluster's details from a given kubeconfig
|
|
func KubeconfigRemoveCluster(ctx context.Context, cluster *k3d.Cluster, kubeconfig *clientcmdapi.Config) *clientcmdapi.Config {
|
|
clusterName := fmt.Sprintf("%s-%s", k3d.DefaultObjectNamePrefix, cluster.Name)
|
|
contextName := fmt.Sprintf("%s-%s", k3d.DefaultObjectNamePrefix, cluster.Name)
|
|
authInfoName := fmt.Sprintf("admin@%s-%s", k3d.DefaultObjectNamePrefix, cluster.Name)
|
|
|
|
// delete elements from kubeconfig if they're present
|
|
delete(kubeconfig.Contexts, contextName)
|
|
delete(kubeconfig.Clusters, clusterName)
|
|
delete(kubeconfig.AuthInfos, authInfoName)
|
|
|
|
// set current-context to any other context, if it was set to the given cluster before
|
|
if kubeconfig.CurrentContext == contextName {
|
|
for k := range kubeconfig.Contexts {
|
|
kubeconfig.CurrentContext = k
|
|
break
|
|
}
|
|
// if current-context didn't change, unset it
|
|
if kubeconfig.CurrentContext == contextName {
|
|
kubeconfig.CurrentContext = ""
|
|
}
|
|
}
|
|
return kubeconfig
|
|
}
|
|
|