mirror of
https://github.com/kubernetes-sigs/kind.git
synced 2025-12-01 07:26:05 +07:00
Merge pull request #1029 from BenTheElder/kubeconfig
kubeconfig overhaul
This commit is contained in:
@@ -30,11 +30,12 @@ import (
|
||||
)
|
||||
|
||||
type flagpole struct {
|
||||
Name string
|
||||
Config string
|
||||
ImageName string
|
||||
Retain bool
|
||||
Wait time.Duration
|
||||
Name string
|
||||
Config string
|
||||
ImageName string
|
||||
Retain bool
|
||||
Wait time.Duration
|
||||
Kubeconfig string
|
||||
}
|
||||
|
||||
// NewCommand returns a new cobra.Command for cluster creation
|
||||
@@ -54,6 +55,7 @@ func NewCommand() *cobra.Command {
|
||||
cmd.Flags().StringVar(&flags.ImageName, "image", "", "node docker image to use for booting the cluster")
|
||||
cmd.Flags().BoolVar(&flags.Retain, "retain", false, "retain nodes for debugging when cluster creation fails")
|
||||
cmd.Flags().DurationVar(&flags.Wait, "wait", time.Duration(0), "Wait for control plane node to be ready (default 0s)")
|
||||
cmd.Flags().StringVar(&flags.Kubeconfig, "kubeconfig", "", "sets kubeconfig path instead of $KUBECONFIG or $HOME/.kube/config")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -77,6 +79,7 @@ func runE(flags *flagpole) error {
|
||||
create.WithNodeImage(flags.ImageName),
|
||||
create.Retain(flags.Retain),
|
||||
create.WaitForReady(flags.Wait),
|
||||
create.WithKubeconfigPath(flags.Kubeconfig),
|
||||
); err != nil {
|
||||
if errs := errors.Errors(err); errs != nil {
|
||||
for _, problem := range errs {
|
||||
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
)
|
||||
|
||||
type flagpole struct {
|
||||
Name string
|
||||
Retain bool
|
||||
Name string
|
||||
Kubeconfig string
|
||||
}
|
||||
|
||||
// NewCommand returns a new cobra.Command for cluster creation
|
||||
@@ -45,13 +45,14 @@ func NewCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&flags.Name, "name", cluster.DefaultName, "the cluster name")
|
||||
cmd.Flags().StringVar(&flags.Kubeconfig, "kubeconfig", "", "sets kubeconfig path instead of $KUBECONFIG or $HOME/.kube/config")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runE(flags *flagpole) error {
|
||||
// Delete the cluster
|
||||
fmt.Printf("Deleting cluster %q ...\n", flags.Name)
|
||||
if err := cluster.NewProvider().Delete(flags.Name); err != nil {
|
||||
if err := cluster.NewProvider().Delete(flags.Name, flags.Kubeconfig); err != nil {
|
||||
return errors.Wrap(err, "failed to delete cluster")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
|
||||
"sigs.k8s.io/kind/cmd/kind/get/clusters"
|
||||
"sigs.k8s.io/kind/cmd/kind/get/kubeconfig"
|
||||
"sigs.k8s.io/kind/cmd/kind/get/kubeconfigpath"
|
||||
"sigs.k8s.io/kind/cmd/kind/get/nodes"
|
||||
)
|
||||
|
||||
@@ -32,13 +31,12 @@ func NewCommand() *cobra.Command {
|
||||
Args: cobra.NoArgs,
|
||||
// TODO(bentheelder): more detailed usage
|
||||
Use: "get",
|
||||
Short: "Gets one of [clusters, nodes, kubeconfig, kubeconfig-path]",
|
||||
Long: "Gets one of [clusters, nodes, kubeconfig, kubeconfig-path]",
|
||||
Short: "Gets one of [clusters, nodes, kubeconfig]",
|
||||
Long: "Gets one of [clusters, nodes, kubeconfig]",
|
||||
}
|
||||
// add subcommands
|
||||
cmd.AddCommand(clusters.NewCommand())
|
||||
cmd.AddCommand(nodes.NewCommand())
|
||||
cmd.AddCommand(kubeconfig.NewCommand())
|
||||
cmd.AddCommand(kubeconfigpath.NewCommand())
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes 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 kubeconfigpath implements the `kubeconfig-path` command
|
||||
package kubeconfigpath
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/cluster"
|
||||
)
|
||||
|
||||
type flagpole struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// NewCommand returns a new cobra.Command for getting the kubeconfig path
|
||||
func NewCommand() *cobra.Command {
|
||||
flags := &flagpole{}
|
||||
cmd := &cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
// TODO(bentheelder): more detailed usage
|
||||
Use: "kubeconfig-path",
|
||||
Short: "prints the default kubeconfig path for the kind cluster by --name",
|
||||
Long: "prints the default kubeconfig path for the kind cluster by --name",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runE(flags)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(
|
||||
&flags.Name,
|
||||
"name",
|
||||
cluster.DefaultName,
|
||||
"the cluster context name",
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runE(flags *flagpole) error {
|
||||
fmt.Println(cluster.NewProvider().KubeConfigPath(flags.Name))
|
||||
return nil
|
||||
}
|
||||
@@ -95,10 +95,6 @@ EOF
|
||||
|
||||
# run e2es with ginkgo-e2e.sh
|
||||
run_tests() {
|
||||
# export the KUBECONFIG
|
||||
KUBECONFIG="$(kind get kubeconfig-path)"
|
||||
export KUBECONFIG
|
||||
|
||||
# IPv6 clusters need some CoreDNS changes in order to work in k8s CI:
|
||||
# 1. k8s CI doesn´t offer IPv6 connectivity, so CoreDNS should be configured
|
||||
# to work in an offline environment:
|
||||
@@ -164,6 +160,11 @@ main() {
|
||||
export ARTIFACTS="${ARTIFACTS:-${PWD}/_artifacts}"
|
||||
mkdir -p "${ARTIFACTS}"
|
||||
|
||||
# export the KUBECONFIG to a unique path for testing
|
||||
KUBECONFIG="${HOME}/.kube/kind-test-config"
|
||||
export KUBECONFIG
|
||||
echo "exported KUBECONFIG=${KUBECONFIG}"
|
||||
|
||||
# debug kind version
|
||||
kind version
|
||||
|
||||
|
||||
@@ -78,3 +78,12 @@ func SetupKubernetes(setupKubernetes bool) ClusterOption {
|
||||
return o, nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithKubeconfigPath sets the explicit --kubeconfig path
|
||||
// The rules from https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands apply
|
||||
func WithKubeconfigPath(kubeconfigPath string) ClusterOption {
|
||||
return func(o *internaltypes.ClusterOptions) (*internaltypes.ClusterOptions, error) {
|
||||
o.KubeconfigPath = kubeconfigPath
|
||||
return o, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,19 +17,14 @@ limitations under the License.
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/cluster/constants"
|
||||
"sigs.k8s.io/kind/pkg/cluster/create"
|
||||
"sigs.k8s.io/kind/pkg/cluster/nodes"
|
||||
"sigs.k8s.io/kind/pkg/errors"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/cluster/nodeutils"
|
||||
internalcontext "sigs.k8s.io/kind/pkg/internal/cluster/context"
|
||||
internalcreate "sigs.k8s.io/kind/pkg/internal/cluster/create"
|
||||
internaldelete "sigs.k8s.io/kind/pkg/internal/cluster/delete"
|
||||
"sigs.k8s.io/kind/pkg/internal/cluster/kubeconfig"
|
||||
internallogs "sigs.k8s.io/kind/pkg/internal/cluster/logs"
|
||||
"sigs.k8s.io/kind/pkg/internal/cluster/providers/docker"
|
||||
internalprovider "sigs.k8s.io/kind/pkg/internal/cluster/providers/provider"
|
||||
@@ -69,8 +64,8 @@ func (p *Provider) Create(name string, options ...create.ClusterOption) error {
|
||||
}
|
||||
|
||||
// Delete tears down a kubernetes-in-docker cluster
|
||||
func (p *Provider) Delete(name string) error {
|
||||
return internaldelete.Cluster(p.ic(name))
|
||||
func (p *Provider) Delete(name, explicitKubeconfigPath string) error {
|
||||
return internaldelete.Cluster(p.ic(name), explicitKubeconfigPath)
|
||||
}
|
||||
|
||||
// List returns a list of clusters for which nodes exist
|
||||
@@ -78,49 +73,11 @@ func (p *Provider) List() ([]string, error) {
|
||||
return p.provider.ListClusters()
|
||||
}
|
||||
|
||||
// KubeConfigPath returns the path to where the Kubeconfig would be placed
|
||||
// by kind based on the configuration.
|
||||
func (p *Provider) KubeConfigPath(name string) string {
|
||||
return p.ic(name).KubeConfigPath()
|
||||
}
|
||||
|
||||
// KubeConfig returns the KUBECONFIG for the cluster
|
||||
// If internal is true, this will contain the internal IP etc.
|
||||
// If internal is fale, this will contain the host IP etc.
|
||||
func (p *Provider) KubeConfig(name string, internal bool) (string, error) {
|
||||
// TODO(bentheelder): move implementation to node provider
|
||||
n, err := p.ic(name).ListNodes()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if internal {
|
||||
var buff bytes.Buffer
|
||||
nodes, err := nodeutils.ControlPlaneNodes(n)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(nodes) < 1 {
|
||||
return "", errors.New("could not locate any control plane nodes")
|
||||
}
|
||||
node := nodes[0]
|
||||
// grab kubeconfig version from one of the control plane nodes
|
||||
if err := node.Command("cat", "/etc/kubernetes/admin.conf").SetStdout(&buff).Run(); err != nil {
|
||||
return "", errors.Wrap(err, "failed to get cluster internal kubeconfig")
|
||||
}
|
||||
return buff.String(), nil
|
||||
}
|
||||
|
||||
// TODO(bentheelder): should not depend on host kubeconfig file!
|
||||
f, err := os.Open(p.KubeConfigPath(name))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get cluster kubeconfig")
|
||||
}
|
||||
defer f.Close()
|
||||
out, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to read kubeconfig")
|
||||
}
|
||||
return string(out), nil
|
||||
return kubeconfig.Get(p.ic(name), internal)
|
||||
}
|
||||
|
||||
// ListNodes returns the list of container IDs for the "nodes" in the cluster
|
||||
|
||||
@@ -19,15 +19,11 @@ limitations under the License.
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/cluster/constants"
|
||||
"sigs.k8s.io/kind/pkg/cluster/nodes"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/internal/cluster/providers/docker"
|
||||
"sigs.k8s.io/kind/pkg/internal/cluster/providers/provider"
|
||||
"sigs.k8s.io/kind/pkg/internal/util/env"
|
||||
)
|
||||
|
||||
// Context is the private shared context underlying pkg/cluster.Context
|
||||
@@ -74,23 +70,6 @@ func (c *Context) GetAPIServerEndpoint() (string, error) {
|
||||
return c.provider.GetAPIServerEndpoint(c.Name())
|
||||
}
|
||||
|
||||
// KubeConfigPath returns the path to where the Kubeconfig would be placed
|
||||
// by kind based on the configuration.
|
||||
func (c *Context) KubeConfigPath() string {
|
||||
// configDir matches the standard directory expected by kubectl etc
|
||||
configDir := filepath.Join(env.HomeDir(), ".kube")
|
||||
// note that the file name however does not, we do not want to overwrite
|
||||
// the standard config, though in the future we may (?) merge them
|
||||
fileName := fmt.Sprintf("kind-config-%s", c.Name())
|
||||
return filepath.Join(configDir, fileName)
|
||||
}
|
||||
|
||||
// ClusterLabel returns the docker object label that will be applied
|
||||
// to cluster "node" containers
|
||||
func (c *Context) ClusterLabel() string {
|
||||
return fmt.Sprintf("%s=%s", constants.ClusterLabelKey, c.Name())
|
||||
}
|
||||
|
||||
// ListNodes returns the list of container IDs for the "nodes" in the cluster
|
||||
func (c *Context) ListNodes() ([]nodes.Node, error) {
|
||||
return c.provider.ListNodes(c.name)
|
||||
|
||||
@@ -18,15 +18,8 @@ limitations under the License.
|
||||
package kubeadminit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/cluster/nodes"
|
||||
"sigs.k8s.io/kind/pkg/errors"
|
||||
"sigs.k8s.io/kind/pkg/exec"
|
||||
"sigs.k8s.io/kind/pkg/globals"
|
||||
@@ -106,20 +99,6 @@ func (a *action) Execute(ctx *actions.ActionContext) error {
|
||||
}
|
||||
}
|
||||
|
||||
// copies the kubeconfig files locally in order to make the cluster
|
||||
// usable with kubectl.
|
||||
// the kubeconfig file created by kubeadm internally to the node
|
||||
// must be modified in order to use the random host port reserved
|
||||
// for the API server and exposed by the node
|
||||
endpoint, err := ctx.ClusterContext.GetAPIServerEndpoint()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get api server endpoint from node")
|
||||
}
|
||||
kubeConfigPath := ctx.ClusterContext.KubeConfigPath()
|
||||
if err := writeKubeConfig(node, kubeConfigPath, endpoint); err != nil {
|
||||
return errors.Wrap(err, "failed to get kubeconfig from node")
|
||||
}
|
||||
|
||||
// if we are only provisioning one node, remove the master taint
|
||||
// https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/#master-isolation
|
||||
if len(allNodes) == 1 {
|
||||
@@ -135,42 +114,3 @@ func (a *action) Execute(ctx *actions.ActionContext) error {
|
||||
ctx.Status.End(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// matches kubeconfig server entry like:
|
||||
// server: https://172.17.0.2:6443
|
||||
// which we rewrite to:
|
||||
// server: https://$ADDRESS:$PORT
|
||||
var serverAddressRE = regexp.MustCompile(`^(\s+server:) https://.*:\d+$`)
|
||||
|
||||
// writeKubeConfig writes a fixed KUBECONFIG to dest
|
||||
// this should only be called on a control plane node
|
||||
// While copyng to the host machine the control plane address
|
||||
// is replaced with local host and the control plane port with
|
||||
// a randomly generated port reserved during node creation.
|
||||
func writeKubeConfig(n nodes.Node, dest string, endpoint string) error {
|
||||
cmd := n.Command("cat", "/etc/kubernetes/admin.conf")
|
||||
lines, err := exec.CombinedOutputLines(cmd)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get kubeconfig from node")
|
||||
}
|
||||
|
||||
// fix the config file, swapping out the server for the forwarded localhost:port
|
||||
var buff bytes.Buffer
|
||||
for _, line := range lines {
|
||||
match := serverAddressRE.FindStringSubmatch(line)
|
||||
if len(match) > 1 {
|
||||
line = fmt.Sprintf("%s https://%s", match[1], endpoint)
|
||||
}
|
||||
buff.WriteString(line)
|
||||
buff.WriteString("\n")
|
||||
}
|
||||
|
||||
// create the directory to contain the KUBECONFIG file.
|
||||
// 0755 is taken from client-go's config handling logic: https://github.com/kubernetes/client-go/blob/5d107d4ebc00ee0ea606ad7e39fd6ce4b0d9bf9e/tools/clientcmd/loader.go#L412
|
||||
err = os.MkdirAll(filepath.Dir(dest), 0755)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create kubeconfig output directory")
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(dest, buff.Bytes(), 0600)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ package create
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"runtime"
|
||||
|
||||
"github.com/alessio/shellescape"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/internal/cluster/create/actions"
|
||||
|
||||
@@ -40,6 +41,7 @@ import (
|
||||
"sigs.k8s.io/kind/pkg/internal/cluster/create/actions/kubeadmjoin"
|
||||
"sigs.k8s.io/kind/pkg/internal/cluster/create/actions/loadbalancer"
|
||||
"sigs.k8s.io/kind/pkg/internal/cluster/create/actions/waitforready"
|
||||
"sigs.k8s.io/kind/pkg/internal/cluster/kubeconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -87,7 +89,7 @@ func Cluster(ctx *context.Context, options ...create.ClusterOption) error {
|
||||
// In case of errors nodes are deleted (except if retain is explicitly set)
|
||||
globals.GetLogger().Errorf("%v", err)
|
||||
if !opts.Retain {
|
||||
_ = delete.Cluster(ctx)
|
||||
_ = delete.Cluster(ctx, opts.KubeconfigPath)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -120,21 +122,36 @@ func Cluster(ctx *context.Context, options ...create.ClusterOption) error {
|
||||
for _, action := range actionsToRun {
|
||||
if err := action.Execute(actionsContext); err != nil {
|
||||
if !opts.Retain {
|
||||
_ = delete.Cluster(ctx)
|
||||
_ = delete.Cluster(ctx, opts.KubeconfigPath)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !opts.SetupKubernetes {
|
||||
// prints how to manually setup the cluster
|
||||
printSetupInstruction(ctx.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
// print how to set KUBECONFIG to point to the cluster etc.
|
||||
printUsage(ctx.Name())
|
||||
return exportKubeconfig(ctx, opts.KubeconfigPath)
|
||||
}
|
||||
|
||||
// exportKubeconfig exports the cluster's kubeconfig and prints usage
|
||||
func exportKubeconfig(ctx *context.Context, kubeconfigPath string) error {
|
||||
// actually export KUBECONFIG
|
||||
if err := kubeconfig.Export(ctx, kubeconfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// construct a sample command for interacting with the cluster
|
||||
kctx := kubeconfig.Context(ctx.Name())
|
||||
sampleCommand := fmt.Sprintf("kubectl cluster-info --context %s", kctx)
|
||||
if kubeconfigPath != "" {
|
||||
// explicit path, include this
|
||||
sampleCommand += " --kubeconfig " + shellescape.Quote(kubeconfigPath)
|
||||
}
|
||||
|
||||
globals.GetLogger().V(0).Infof(`Set kubectl context to "%s"`, kctx)
|
||||
globals.GetLogger().V(0).Infof("You can now use your cluster with:\n\n" + sampleCommand)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -177,41 +194,3 @@ func collectOptions(options ...create.ClusterOption) (*createtypes.ClusterOption
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func printUsage(name string) {
|
||||
// TODO: consider shell detection.
|
||||
if runtime.GOOS == "windows" {
|
||||
fmt.Printf(
|
||||
"Cluster creation complete. To setup KUBECONFIG:\n\n"+
|
||||
|
||||
"For the default cmd.exe console call:\n"+
|
||||
"kind get kubeconfig-path > kindpath\n"+
|
||||
"set /p KUBECONFIG=<kindpath && del kindpath\n\n"+
|
||||
|
||||
"for PowerShell call:\n"+
|
||||
"$env:KUBECONFIG=\"$(kind get kubeconfig-path --name=%[1]q)\"\n\n"+
|
||||
|
||||
"For bash on Windows:\n"+
|
||||
"export KUBECONFIG=\"$(kind get kubeconfig-path --name=%[1]q)\"\n\n"+
|
||||
|
||||
"You can now use the cluster:\n"+
|
||||
"kubectl cluster-info\n",
|
||||
name,
|
||||
)
|
||||
} else {
|
||||
fmt.Printf(
|
||||
"Cluster creation complete. You can now use the cluster with:\n\n"+
|
||||
|
||||
"export KUBECONFIG=\"$(kind get kubeconfig-path --name=%q)\"\n"+
|
||||
"kubectl cluster-info\n",
|
||||
name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func printSetupInstruction(name string) {
|
||||
fmt.Printf(
|
||||
"Nodes creation complete. You can now setup kubernetes using docker exec %s-<node> kubeadm ...\n",
|
||||
name,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,9 +31,10 @@ import (
|
||||
type ClusterOptions struct {
|
||||
Config *config.Cluster
|
||||
// NodeImage overrides the nodes' images in Config if non-zero
|
||||
NodeImage string
|
||||
Retain bool
|
||||
WaitForReady time.Duration
|
||||
NodeImage string
|
||||
Retain bool
|
||||
WaitForReady time.Duration
|
||||
KubeconfigPath string
|
||||
//TODO: Refactor this. It is a temporary solution for a phased breakdown of different
|
||||
// operations, specifically create. see https://github.com/kubernetes-sigs/kind/issues/324
|
||||
SetupKubernetes bool // if kind should setup kubernetes after creating nodes
|
||||
|
||||
@@ -17,33 +17,33 @@ limitations under the License.
|
||||
package delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/errors"
|
||||
"sigs.k8s.io/kind/pkg/globals"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/internal/cluster/context"
|
||||
"sigs.k8s.io/kind/pkg/internal/util/kubeconfig"
|
||||
)
|
||||
|
||||
// Cluster deletes the cluster identified by ctx
|
||||
func Cluster(c *context.Context) error {
|
||||
// explicitKubeconfigPath is --kubeconfig, following the rules from
|
||||
// https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands
|
||||
func Cluster(c *context.Context, explicitKubeconfigPath string) error {
|
||||
n, err := c.ListNodes()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error listing nodes")
|
||||
}
|
||||
|
||||
// try to remove the kind kube config file generated by "kind create cluster"
|
||||
err = os.Remove(c.KubeConfigPath())
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
globals.GetLogger().Warnf("Tried to remove %s but received error: %s\n", c.KubeConfigPath(), err)
|
||||
kerr := kubeconfig.RemoveKIND(c.Name(), explicitKubeconfigPath)
|
||||
if kerr != nil {
|
||||
globals.GetLogger().Errorf("failed to update kubeconfig: %v", kerr)
|
||||
}
|
||||
|
||||
// check if $KUBECONFIG is set and let the user know to unset if so
|
||||
if strings.Contains(os.Getenv("KUBECONFIG"), c.KubeConfigPath()) {
|
||||
fmt.Printf("$KUBECONFIG is still set to use %s even though that file has been deleted, remember to unset it\n", c.KubeConfigPath())
|
||||
err = c.Provider().DeleteNodes(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Provider().DeleteNodes(n)
|
||||
if kerr != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
90
pkg/internal/cluster/kubeconfig/kubeconfig.go
Normal file
90
pkg/internal/cluster/kubeconfig/kubeconfig.go
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/cluster/nodeutils"
|
||||
"sigs.k8s.io/kind/pkg/errors"
|
||||
"sigs.k8s.io/kind/pkg/internal/cluster/context"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/internal/util/kubeconfig"
|
||||
)
|
||||
|
||||
// Export exports the kubeconfig given the cluster context and a path to write it to
|
||||
// This will always be an external kubeconfig
|
||||
func Export(ctx *context.Context, explicitPath string) error {
|
||||
cfg, err := get(ctx, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return kubeconfig.WriteMerged(cfg, explicitPath)
|
||||
}
|
||||
|
||||
// Get returns the kubeconfig for the cluster
|
||||
// external controls if the internal IP address is used or the host endpoint
|
||||
func Get(ctx *context.Context, external bool) (string, error) {
|
||||
cfg, err := get(ctx, external)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b, err := kubeconfig.Encode(cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
func Context(kindClusterName string) string {
|
||||
return kubeconfig.KINDClusterKey(kindClusterName)
|
||||
}
|
||||
|
||||
func get(ctx *context.Context, external bool) (*kubeconfig.Config, error) {
|
||||
// find a control plane node to get the kubeadm config from
|
||||
n, err := ctx.ListNodes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buff bytes.Buffer
|
||||
nodes, err := nodeutils.ControlPlaneNodes(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(nodes) < 1 {
|
||||
return nil, errors.New("could not locate any control plane nodes")
|
||||
}
|
||||
node := nodes[0]
|
||||
|
||||
// grab kubeconfig version from the node
|
||||
if err := node.Command("cat", "/etc/kubernetes/admin.conf").SetStdout(&buff).Run(); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get cluster internal kubeconfig")
|
||||
}
|
||||
|
||||
// if we're doing external we need to override the server endpoint
|
||||
server := ""
|
||||
if external {
|
||||
endpoint, err := ctx.GetAPIServerEndpoint()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server = "https://" + endpoint
|
||||
}
|
||||
|
||||
// actually encode
|
||||
return kubeconfig.KINDFromRawKubeadm(buff.String(), ctx.Name(), server)
|
||||
}
|
||||
64
pkg/internal/util/kubeconfig/encode.go
Normal file
64
pkg/internal/util/kubeconfig/encode.go
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
kubeyaml "sigs.k8s.io/yaml"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/errors"
|
||||
)
|
||||
|
||||
// Encode encodes the cfg to yaml
|
||||
func Encode(cfg *Config) ([]byte, error) {
|
||||
// NOTE: kubernetes's yaml library doesn't handle inline fields very well
|
||||
// so we're not using that to marshal
|
||||
encoded, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to encode KUBECONFIG")
|
||||
}
|
||||
|
||||
// normalize with kubernetes's yaml library
|
||||
// this is not strictly necessary, but it ensures minimal diffs when
|
||||
// modifying kubeconfig files, which is nice to have
|
||||
encoded, err = normYaml(encoded)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to normalize KUBECONFIG encoding")
|
||||
}
|
||||
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
// normYaml round trips yaml bytes through sigs.k8s.io/yaml to normalize them
|
||||
// versus other kuberernetes ecosystem yaml output
|
||||
func normYaml(y []byte) ([]byte, error) {
|
||||
var unstructured interface{}
|
||||
if err := kubeyaml.Unmarshal(y, &unstructured); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encoded, err := kubeyaml.Marshal(&unstructured)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// special case: don't write anything when empty
|
||||
if bytes.Equal(encoded, []byte("{}\n")) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
return encoded, nil
|
||||
}
|
||||
64
pkg/internal/util/kubeconfig/encode_test.go
Normal file
64
pkg/internal/util/kubeconfig/encode_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/internal/util/assert"
|
||||
)
|
||||
|
||||
func TestEncodeRoundtrip(t *testing.T) {
|
||||
// test round tripping a kubeconfig
|
||||
const aConfig = `apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: definitlyacert
|
||||
server: https://192.168.9.4:6443
|
||||
name: kind-kind
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kind-kind
|
||||
user: kind-kind
|
||||
name: kind-kind
|
||||
current-context: kind-kind
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: kind-kind
|
||||
user:
|
||||
client-certificate-data: seemslegit
|
||||
client-key-data: yup
|
||||
`
|
||||
cfg, err := KINDFromRawKubeadm(aConfig, "kind", "")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode kubeconfig: %v", err)
|
||||
}
|
||||
encoded, err := Encode(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode kubeconfig: %v", err)
|
||||
}
|
||||
assert.StringEqual(t, aConfig, string(encoded))
|
||||
}
|
||||
|
||||
func TestEncodeEmpty(t *testing.T) {
|
||||
encoded, err := Encode(&Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode kubeconfig: %v", err)
|
||||
}
|
||||
assert.StringEqual(t, "", string(encoded))
|
||||
}
|
||||
41
pkg/internal/util/kubeconfig/helpers.go
Normal file
41
pkg/internal/util/kubeconfig/helpers.go
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kind/pkg/errors"
|
||||
)
|
||||
|
||||
// KINDClusterKey identifies kind clusters in kubeconfig files
|
||||
func KINDClusterKey(clusterName string) string {
|
||||
return "kind-" + clusterName
|
||||
}
|
||||
|
||||
// checkKubeadmExpectations validates that a kubeadm created KUBECONFIG meets
|
||||
// our expectations, namely on the number of entries
|
||||
func checkKubeadmExpectations(cfg *Config) error {
|
||||
if len(cfg.Clusters) != 1 {
|
||||
return errors.Errorf("kubeadm KUBECONFIG should have one cluster, but read %d", len(cfg.Clusters))
|
||||
}
|
||||
if len(cfg.Users) != 1 {
|
||||
return errors.Errorf("kubeadm KUBECONFIG should have one user, but read %d", len(cfg.Users))
|
||||
}
|
||||
if len(cfg.Contexts) != 1 {
|
||||
return errors.Errorf("kubeadm KUBECONFIG should have one context, but read %d", len(cfg.Contexts))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
88
pkg/internal/util/kubeconfig/helpers_test.go
Normal file
88
pkg/internal/util/kubeconfig/helpers_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/internal/util/assert"
|
||||
)
|
||||
|
||||
func TestKINDClusterKey(t *testing.T) {
|
||||
assert.StringEqual(t, "kind-foobar", KINDClusterKey("foobar"))
|
||||
}
|
||||
|
||||
func TestCheckKubeadmExpectations(t *testing.T) {
|
||||
cases := []struct {
|
||||
Name string
|
||||
Config *Config
|
||||
ExpectError bool
|
||||
}{
|
||||
{
|
||||
Name: "too many of all entries",
|
||||
Config: &Config{
|
||||
Clusters: make([]NamedCluster, 5),
|
||||
Contexts: make([]NamedContext, 5),
|
||||
Users: make([]NamedUser, 5),
|
||||
},
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "too many users",
|
||||
Config: &Config{
|
||||
Clusters: make([]NamedCluster, 1),
|
||||
Contexts: make([]NamedContext, 1),
|
||||
Users: make([]NamedUser, 2),
|
||||
},
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "too many clusters",
|
||||
Config: &Config{
|
||||
Clusters: make([]NamedCluster, 2),
|
||||
Contexts: make([]NamedContext, 1),
|
||||
Users: make([]NamedUser, 1),
|
||||
},
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "too many contexts",
|
||||
Config: &Config{
|
||||
Clusters: make([]NamedCluster, 1),
|
||||
Contexts: make([]NamedContext, 2),
|
||||
Users: make([]NamedUser, 1),
|
||||
},
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "just right",
|
||||
Config: &Config{
|
||||
Clusters: make([]NamedCluster, 1),
|
||||
Contexts: make([]NamedContext, 1),
|
||||
Users: make([]NamedUser, 1),
|
||||
},
|
||||
ExpectError: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.ExpectError(t, tc.ExpectError, checkKubeadmExpectations(tc.Config))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package env
|
||||
package kubeconfig
|
||||
|
||||
// NOTE this is from client-go. Rather than pull in client-go for this one
|
||||
// standalone method, we have an (unmodified) fork here.
|
||||
@@ -26,13 +26,13 @@ import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// HomeDir returns the home directory for the current user.
|
||||
// homeDir returns the home directory for the current user.
|
||||
// On Windows:
|
||||
// 1. the first of %HOME%, %HOMEDRIVE%%HOMEPATH%, %USERPROFILE% containing a `.kube\config` file is returned.
|
||||
// 2. if none of those locations contain a `.kube\config` file, the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists and is writeable is returned.
|
||||
// 3. if none of those locations are writeable, the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists is returned.
|
||||
// 4. if none of those locations exists, the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that is set is returned.
|
||||
func HomeDir() string {
|
||||
func homeDir() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
home := os.Getenv("HOME")
|
||||
homeDriveHomePath := ""
|
||||
49
pkg/internal/util/kubeconfig/lock.go
Normal file
49
pkg/internal/util/kubeconfig/lock.go
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// these are from
|
||||
// https://github.com/kubernetes/client-go/blob/611184f7c43ae2d520727f01d49620c7ed33412d/tools/clientcmd/loader.go#L439-L440
|
||||
|
||||
func lockFile(filename string) error {
|
||||
// Make sure the dir exists before we try to create a lock file.
|
||||
dir := filepath.Dir(filename)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
f, err := os.OpenFile(lockName(filename), os.O_CREATE|os.O_EXCL, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func unlockFile(filename string) error {
|
||||
return os.Remove(lockName(filename))
|
||||
}
|
||||
|
||||
func lockName(filename string) string {
|
||||
return filename + ".lock"
|
||||
}
|
||||
100
pkg/internal/util/kubeconfig/merge.go
Normal file
100
pkg/internal/util/kubeconfig/merge.go
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kind/pkg/errors"
|
||||
)
|
||||
|
||||
// WriteMerged writes a kind kubeconfig (see KINDFromRawKubeadm) into configPath
|
||||
// merging with the existing contents if any and setting the current context to
|
||||
// the kind config's current context.
|
||||
func WriteMerged(kindConfig *Config, explicitConfigPath string) error {
|
||||
// figure out what filepath we should use
|
||||
configPath := pathForMerge(explicitConfigPath)
|
||||
|
||||
// lock config file the same as client-go
|
||||
if err := lockFile(configPath); err != nil {
|
||||
return errors.Wrap(err, "failed to lock config file")
|
||||
}
|
||||
defer func() {
|
||||
_ = unlockFile(configPath)
|
||||
}()
|
||||
|
||||
// read in existing
|
||||
existing, err := read(configPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get kubeconfig to merge")
|
||||
}
|
||||
|
||||
// merge with kind kubeconfig
|
||||
if err := merge(existing, kindConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write back out
|
||||
return write(existing, configPath)
|
||||
}
|
||||
|
||||
// merge kind config into an existing config
|
||||
func merge(existing, kind *Config) error {
|
||||
// verify assumptions about kubeadm / kind kubeconfigs
|
||||
if err := checkKubeadmExpectations(kind); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert or append cluster entry
|
||||
shouldAppend := true
|
||||
for i := range existing.Clusters {
|
||||
if existing.Clusters[i].Name == kind.Clusters[0].Name {
|
||||
existing.Clusters[i] = kind.Clusters[0]
|
||||
shouldAppend = false
|
||||
}
|
||||
}
|
||||
if shouldAppend {
|
||||
existing.Clusters = append(existing.Clusters, kind.Clusters[0])
|
||||
}
|
||||
|
||||
// insert or append user entry
|
||||
shouldAppend = true
|
||||
for i := range existing.Users {
|
||||
if existing.Users[i].Name == kind.Users[0].Name {
|
||||
existing.Users[i] = kind.Users[0]
|
||||
shouldAppend = false
|
||||
}
|
||||
}
|
||||
if shouldAppend {
|
||||
existing.Users = append(existing.Users, kind.Users[0])
|
||||
}
|
||||
|
||||
// insert or append context entry
|
||||
shouldAppend = true
|
||||
for i := range existing.Contexts {
|
||||
if existing.Contexts[i].Name == kind.Contexts[0].Name {
|
||||
existing.Contexts[i] = kind.Contexts[0]
|
||||
shouldAppend = false
|
||||
}
|
||||
}
|
||||
if shouldAppend {
|
||||
existing.Contexts = append(existing.Contexts, kind.Contexts[0])
|
||||
}
|
||||
|
||||
// set the current context
|
||||
existing.CurrentContext = kind.CurrentContext
|
||||
|
||||
return nil
|
||||
}
|
||||
442
pkg/internal/util/kubeconfig/merge_test.go
Normal file
442
pkg/internal/util/kubeconfig/merge_test.go
Normal file
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/internal/util/assert"
|
||||
)
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
cases := []struct {
|
||||
Name string
|
||||
Existing *Config
|
||||
Kind *Config
|
||||
Expected *Config
|
||||
ExpectError bool
|
||||
}{
|
||||
{
|
||||
Name: "bad kind config",
|
||||
Existing: &Config{},
|
||||
Kind: &Config{},
|
||||
Expected: &Config{},
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "empty existing",
|
||||
Existing: &Config{},
|
||||
Kind: &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
},
|
||||
Expected: &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectError: false,
|
||||
},
|
||||
{
|
||||
Name: "replace existing",
|
||||
Existing: &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
Cluster: Cluster{
|
||||
Server: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
},
|
||||
Kind: &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
},
|
||||
Expected: &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectError: false,
|
||||
},
|
||||
{
|
||||
Name: "add to existing",
|
||||
Existing: &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kops-blah",
|
||||
Cluster: Cluster{
|
||||
Server: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kops-blah",
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kops-blah",
|
||||
},
|
||||
},
|
||||
},
|
||||
Kind: &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
},
|
||||
Expected: &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kops-blah",
|
||||
Cluster: Cluster{
|
||||
Server: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kops-blah",
|
||||
},
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kops-blah",
|
||||
},
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectError: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := merge(tc.Existing, tc.Kind)
|
||||
assert.ExpectError(t, tc.ExpectError, err)
|
||||
if !tc.ExpectError && !reflect.DeepEqual(tc.Existing, tc.Expected) {
|
||||
t.Errorf("Merged Config did not equal Expected")
|
||||
t.Errorf("Expected: %+v", tc.Expected)
|
||||
t.Errorf("Actual: %+v", tc.Existing)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteMerged(t *testing.T) {
|
||||
t.Run("normal merge", testWriteMergedNormal)
|
||||
t.Run("bad kind config", testWriteMergedBogusConfig)
|
||||
t.Run("merge into non-existent file", testWriteMergedNoExistingFile)
|
||||
}
|
||||
|
||||
func testWriteMergedNormal(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir, err := ioutil.TempDir("", "kind-testwritemerged")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create tempdir: %d", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// create an existing kubeconfig
|
||||
const existingConfig = `clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: definitelyacert
|
||||
server: https://192.168.9.4:6443
|
||||
name: kind-foo
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kind-foo
|
||||
user: kind-foo
|
||||
name: kind-foo
|
||||
current-context: kind-foo
|
||||
kind: Config
|
||||
apiVersion: v1
|
||||
preferences: {}
|
||||
users:
|
||||
- name: kind-foo
|
||||
user:
|
||||
client-certificate-data: seemslegit
|
||||
client-key-data: yep
|
||||
`
|
||||
existingConfigPath := filepath.Join(dir, "existing-kubeconfig")
|
||||
if err := ioutil.WriteFile(existingConfigPath, []byte(existingConfig), os.ModePerm); err != nil {
|
||||
t.Fatalf("Failed to create existing kubeconfig: %d", err)
|
||||
}
|
||||
|
||||
kindConfig := &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
Cluster: Cluster{
|
||||
Server: "https://127.0.0.1:6443",
|
||||
OtherFields: map[string]interface{}{
|
||||
"certificate-authority-data": "definitelyacert",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
Context: Context{
|
||||
User: "kind-kind",
|
||||
Cluster: "kind-kind",
|
||||
},
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
User: map[string]interface{}{
|
||||
"client-certificate-data": "seemslegit",
|
||||
"client-key-data": "yep",
|
||||
},
|
||||
},
|
||||
},
|
||||
CurrentContext: "kind-kind",
|
||||
OtherFields: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Config",
|
||||
"preferences": map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
// ensure that we can write this merged config
|
||||
if err := WriteMerged(kindConfig, existingConfigPath); err != nil {
|
||||
t.Fatalf("Failed to write merged kubeconfig: %v", err)
|
||||
}
|
||||
|
||||
// ensure the output matches expected
|
||||
f, err := os.Open(existingConfigPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open merged kubeconfig: %v", err)
|
||||
}
|
||||
contents, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read merged kubeconfig: %v", err)
|
||||
}
|
||||
expected := `apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: definitelyacert
|
||||
server: https://192.168.9.4:6443
|
||||
name: kind-foo
|
||||
- cluster:
|
||||
certificate-authority-data: definitelyacert
|
||||
server: https://127.0.0.1:6443
|
||||
name: kind-kind
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kind-foo
|
||||
user: kind-foo
|
||||
name: kind-foo
|
||||
- context:
|
||||
cluster: kind-kind
|
||||
user: kind-kind
|
||||
name: kind-kind
|
||||
current-context: kind-kind
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: kind-foo
|
||||
user:
|
||||
client-certificate-data: seemslegit
|
||||
client-key-data: yep
|
||||
- name: kind-kind
|
||||
user:
|
||||
client-certificate-data: seemslegit
|
||||
client-key-data: yep
|
||||
`
|
||||
assert.StringEqual(t, expected, string(contents))
|
||||
}
|
||||
|
||||
func testWriteMergedBogusConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir, err := ioutil.TempDir("", "kind-testwritemerged")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create tempdir: %d", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
err = WriteMerged(&Config{}, filepath.Join(dir, "bogus"))
|
||||
assert.ExpectError(t, true, err)
|
||||
}
|
||||
|
||||
func testWriteMergedNoExistingFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir, err := ioutil.TempDir("", "kind-testwritemerged")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create tempdir: %d", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
kindConfig := &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
Cluster: Cluster{
|
||||
Server: "https://127.0.0.1:6443",
|
||||
OtherFields: map[string]interface{}{
|
||||
"certificate-authority-data": "definitelyacert",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
Context: Context{
|
||||
User: "kind-kind",
|
||||
Cluster: "kind-kind",
|
||||
},
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
User: map[string]interface{}{
|
||||
"client-certificate-data": "seemslegit",
|
||||
"client-key-data": "yep",
|
||||
},
|
||||
},
|
||||
},
|
||||
CurrentContext: "kind-kind",
|
||||
OtherFields: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Config",
|
||||
"preferences": map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
|
||||
nonExistentPath := filepath.Join(dir, "bogus")
|
||||
err = WriteMerged(kindConfig, nonExistentPath)
|
||||
assert.ExpectError(t, false, err)
|
||||
|
||||
// ensure the output matches expected
|
||||
f, err := os.Open(nonExistentPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open merged kubeconfig: %v", err)
|
||||
}
|
||||
contents, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read merged kubeconfig: %v", err)
|
||||
}
|
||||
expected := `clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: definitelyacert
|
||||
server: https://127.0.0.1:6443
|
||||
name: kind-kind
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kind-kind
|
||||
user: kind-kind
|
||||
name: kind-kind
|
||||
current-context: kind-kind
|
||||
users:
|
||||
- name: kind-kind
|
||||
user:
|
||||
client-certificate-data: seemslegit
|
||||
client-key-data: yep
|
||||
`
|
||||
assert.StringEqual(t, expected, string(contents))
|
||||
}
|
||||
93
pkg/internal/util/kubeconfig/paths.go
Normal file
93
pkg/internal/util/kubeconfig/paths.go
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
const kubeconfigEnv = "KUBECONFIG"
|
||||
|
||||
/*
|
||||
paths returns the list of paths to be considered for kubeconfig files
|
||||
where explicitPath is the value of --kubeconfig
|
||||
|
||||
Logic based on kubectl
|
||||
|
||||
https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands
|
||||
|
||||
- If the --kubeconfig flag is set, then only that file is loaded. The flag may only be set once and no merging takes place.
|
||||
|
||||
- If $KUBECONFIG environment variable is set, then it is used as a list of paths (normal path delimiting rules for your system). These paths are merged. When a value is modified, it is modified in the file that defines the stanza. When a value is created, it is created in the first file that exists. - If no files in the chain exist, then it creates the last file in the list.
|
||||
|
||||
- Otherwise, ${HOME}/.kube/config is used and no merging takes place.
|
||||
*/
|
||||
func paths(explicitPath string, getEnv func(string) string) []string {
|
||||
if explicitPath != "" {
|
||||
return []string{explicitPath}
|
||||
}
|
||||
|
||||
paths := discardEmptyAndDuplicates(
|
||||
filepath.SplitList(getEnv(kubeconfigEnv)),
|
||||
)
|
||||
if len(paths) != 0 {
|
||||
return paths
|
||||
}
|
||||
|
||||
return []string{path.Join(homeDir(), ".kube", "config")}
|
||||
}
|
||||
|
||||
// pathForMerge returns the file that kubectl would merge into
|
||||
func pathForMerge(explicitPath string) string {
|
||||
// find the first file that exists
|
||||
p := paths(explicitPath, os.Getenv)
|
||||
if len(p) == 1 {
|
||||
return p[0]
|
||||
}
|
||||
for _, filename := range p {
|
||||
if fileExists(filename) {
|
||||
return filename
|
||||
}
|
||||
}
|
||||
// otherwise the last file
|
||||
return p[len(p)-1]
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
func discardEmptyAndDuplicates(paths []string) []string {
|
||||
seen := sets.NewString()
|
||||
kept := 0
|
||||
for _, p := range paths {
|
||||
if p != "" && !seen.Has(p) {
|
||||
paths[kept] = p
|
||||
kept++
|
||||
seen.Insert(p)
|
||||
}
|
||||
}
|
||||
return paths[:kept]
|
||||
}
|
||||
82
pkg/internal/util/kubeconfig/read.go
Normal file
82
pkg/internal/util/kubeconfig/read.go
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/errors"
|
||||
)
|
||||
|
||||
// KINDFromRawKubeadm returns a kind kubeconfig derived from the raw kubeadm kubeconfig,
|
||||
// the kind clusterName, and the server.
|
||||
// server is ignored if unset.
|
||||
func KINDFromRawKubeadm(rawKubeadmKubeConfig, clusterName, server string) (*Config, error) {
|
||||
cfg := &Config{}
|
||||
if err := yaml.Unmarshal([]byte(rawKubeadmKubeConfig), cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// verify assumptions about kubeadm kubeconfigs
|
||||
if err := checkKubeadmExpectations(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// compute unique kubeconfig key for this cluster
|
||||
key := KINDClusterKey(clusterName)
|
||||
|
||||
// use the unique key for all named references
|
||||
cfg.Clusters[0].Name = key
|
||||
cfg.Users[0].Name = key
|
||||
cfg.Contexts[0].Name = key
|
||||
cfg.Contexts[0].Context.User = key
|
||||
cfg.Contexts[0].Context.Cluster = key
|
||||
cfg.CurrentContext = key
|
||||
|
||||
// patch server field if server was set
|
||||
if server != "" {
|
||||
cfg.Clusters[0].Cluster.Server = server
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// read loads a KUBECONFIG file from configPath
|
||||
func read(configPath string) (*Config, error) {
|
||||
// try to open, return default if no such file
|
||||
f, err := os.Open(configPath)
|
||||
if os.IsNotExist(err) {
|
||||
return &Config{}, nil
|
||||
} else if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
// otherwise read in and deserialize
|
||||
cfg := &Config{}
|
||||
rawExisting, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
if err := yaml.Unmarshal(rawExisting, cfg); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
104
pkg/internal/util/kubeconfig/read_test.go
Normal file
104
pkg/internal/util/kubeconfig/read_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/internal/util/assert"
|
||||
)
|
||||
|
||||
func TestKINDFromRawKubeadm(t *testing.T) {
|
||||
// test that a bogus config is caught
|
||||
t.Run("bad config", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := KINDFromRawKubeadm(" ", "kind", "")
|
||||
assert.ExpectError(t, true, err)
|
||||
})
|
||||
// test reading a legitimate kubeadm config and converting it to a kind config
|
||||
t.Run("valid config", func(t *testing.T) {
|
||||
const rawConfig = `apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: definitelyacert
|
||||
server: https://192.168.9.4:6443
|
||||
name: kind
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kind
|
||||
user: kubernetes-admin
|
||||
name: kubernetes-admin@kind
|
||||
current-context: kubernetes-admin@kind
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: kubernetes-admin
|
||||
user:
|
||||
client-certificate-data: seemslegit
|
||||
client-key-data: yep
|
||||
`
|
||||
server := "https://127.0.0.1:6443"
|
||||
expected := &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
Cluster: Cluster{
|
||||
Server: server,
|
||||
OtherFields: map[string]interface{}{
|
||||
"certificate-authority-data": "definitelyacert",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
Context: Context{
|
||||
User: "kind-kind",
|
||||
Cluster: "kind-kind",
|
||||
},
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
User: map[string]interface{}{
|
||||
"client-certificate-data": "seemslegit",
|
||||
"client-key-data": "yep",
|
||||
},
|
||||
},
|
||||
},
|
||||
CurrentContext: "kind-kind",
|
||||
OtherFields: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Config",
|
||||
"preferences": map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
cfg, err := KINDFromRawKubeadm(rawConfig, "kind", server)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode kubeconfig: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg, expected) {
|
||||
t.Errorf("Read Config did not equal Expected")
|
||||
t.Errorf("Expected: %+v", expected)
|
||||
t.Errorf("Actual: %+v", cfg)
|
||||
t.Errorf("type: %s", reflect.TypeOf(cfg.OtherFields["preferences"]))
|
||||
}
|
||||
})
|
||||
}
|
||||
111
pkg/internal/util/kubeconfig/remove.go
Normal file
111
pkg/internal/util/kubeconfig/remove.go
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/errors"
|
||||
)
|
||||
|
||||
// RemoveKIND removes the kind cluster kindClusterName from the KUBECONFIG
|
||||
// files at configPaths
|
||||
func RemoveKIND(kindClusterName string, explicitPath string) error {
|
||||
// remove kind from each if present
|
||||
for _, configPath := range paths(explicitPath, os.Getenv) {
|
||||
if err := func(configPath string) error {
|
||||
// lock before modifying
|
||||
if err := lockFile(configPath); err != nil {
|
||||
return errors.Wrap(err, "failed to lock config file")
|
||||
}
|
||||
defer func(configPath string) {
|
||||
_ = unlockFile(configPath)
|
||||
}(configPath)
|
||||
|
||||
// read in existing
|
||||
existing, err := read(configPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read kubeconfig to remove KIND entry")
|
||||
}
|
||||
|
||||
// remove the kind cluster from the config
|
||||
if remove(existing, kindClusterName) {
|
||||
// write out the updated config if we modified anything
|
||||
if err := write(existing, configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}(configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// remove drops kindClusterName entries from the cfg
|
||||
func remove(cfg *Config, kindClusterName string) bool {
|
||||
mutated := false
|
||||
|
||||
// get kind cluster identifier
|
||||
key := KINDClusterKey(kindClusterName)
|
||||
|
||||
// filter out kind cluster from clusters
|
||||
kept := 0
|
||||
for _, c := range cfg.Clusters {
|
||||
if c.Name != key {
|
||||
cfg.Clusters[kept] = c
|
||||
kept++
|
||||
} else {
|
||||
mutated = true
|
||||
}
|
||||
}
|
||||
cfg.Clusters = cfg.Clusters[:kept]
|
||||
|
||||
// filter out kind cluster from users
|
||||
kept = 0
|
||||
for _, u := range cfg.Users {
|
||||
if u.Name != key {
|
||||
cfg.Users[kept] = u
|
||||
kept++
|
||||
} else {
|
||||
mutated = true
|
||||
}
|
||||
}
|
||||
cfg.Users = cfg.Users[:kept]
|
||||
|
||||
// filter out kind cluster from contexts
|
||||
kept = 0
|
||||
for _, c := range cfg.Contexts {
|
||||
if c.Name != key {
|
||||
cfg.Contexts[kept] = c
|
||||
kept++
|
||||
} else {
|
||||
mutated = true
|
||||
}
|
||||
}
|
||||
cfg.Contexts = cfg.Contexts[:kept]
|
||||
|
||||
// unset current context if it points to this cluster
|
||||
if cfg.CurrentContext == key {
|
||||
cfg.CurrentContext = ""
|
||||
mutated = true
|
||||
}
|
||||
|
||||
return mutated
|
||||
}
|
||||
291
pkg/internal/util/kubeconfig/remove_test.go
Normal file
291
pkg/internal/util/kubeconfig/remove_test.go
Normal file
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/internal/util/assert"
|
||||
)
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
cases := []struct {
|
||||
Name string
|
||||
Existing *Config
|
||||
ClusterName string
|
||||
Expected *Config
|
||||
ExpectModified bool
|
||||
}{
|
||||
{
|
||||
Name: "empty config",
|
||||
Existing: &Config{},
|
||||
ClusterName: "foo",
|
||||
Expected: &Config{},
|
||||
ExpectModified: false,
|
||||
},
|
||||
{
|
||||
Name: "remove kind from only kind",
|
||||
Existing: &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
},
|
||||
ClusterName: "kind",
|
||||
Expected: &Config{
|
||||
Clusters: []NamedCluster{},
|
||||
Users: []NamedUser{},
|
||||
Contexts: []NamedContext{},
|
||||
},
|
||||
ExpectModified: true,
|
||||
},
|
||||
{
|
||||
Name: "remove kind, leave kops",
|
||||
Existing: &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kops-blah",
|
||||
Cluster: Cluster{
|
||||
Server: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kops-blah",
|
||||
},
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kops-blah",
|
||||
},
|
||||
{
|
||||
Name: "kind-kind",
|
||||
},
|
||||
},
|
||||
CurrentContext: "kind-kind",
|
||||
},
|
||||
ClusterName: "kind",
|
||||
Expected: &Config{
|
||||
Clusters: []NamedCluster{
|
||||
{
|
||||
Name: "kops-blah",
|
||||
Cluster: Cluster{
|
||||
Server: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
Users: []NamedUser{
|
||||
{
|
||||
Name: "kops-blah",
|
||||
},
|
||||
},
|
||||
Contexts: []NamedContext{
|
||||
{
|
||||
Name: "kops-blah",
|
||||
},
|
||||
},
|
||||
CurrentContext: "",
|
||||
},
|
||||
ExpectModified: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
modified := remove(tc.Existing, tc.ClusterName)
|
||||
if modified != tc.ExpectModified {
|
||||
if tc.ExpectModified {
|
||||
t.Errorf("Expected config to be modified but got modified == false")
|
||||
} else {
|
||||
t.Errorf("Expected config to be modified but got modified == true")
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(tc.Existing, tc.Expected) {
|
||||
t.Errorf("Merged Config did not equal Expected")
|
||||
t.Errorf("Expected: %+v", tc.Expected)
|
||||
t.Errorf("Actual: %+v", tc.Existing)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveKIND(t *testing.T) {
|
||||
t.Run("only kind", testRemoveKINDTrivial)
|
||||
t.Run("leave another cluster", testRemoveKINDKeepOther)
|
||||
}
|
||||
|
||||
func testRemoveKINDTrivial(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir, err := ioutil.TempDir("", "kind-testremovekind")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create tempdir: %d", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// create an existing kubeconfig
|
||||
const existingConfig = `clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: definitelyacert
|
||||
server: https://192.168.9.4:6443
|
||||
name: kind-foo
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kind-foo
|
||||
user: kind-foo
|
||||
name: kind-foo
|
||||
current-context: kind-foo
|
||||
kind: Config
|
||||
apiVersion: v1
|
||||
preferences: {}
|
||||
users:
|
||||
- name: kind-foo
|
||||
user:
|
||||
client-certificate-data: seemslegit
|
||||
client-key-data: yep
|
||||
`
|
||||
existingConfigPath := filepath.Join(dir, "existing-kubeconfig")
|
||||
if err := ioutil.WriteFile(existingConfigPath, []byte(existingConfig), os.ModePerm); err != nil {
|
||||
t.Fatalf("Failed to create existing kubeconfig: %d", err)
|
||||
}
|
||||
|
||||
// ensure that we can write this merged config
|
||||
if err := RemoveKIND("foo", existingConfigPath); err != nil {
|
||||
t.Fatalf("Failed to remove kind from kubeconfig: %v", err)
|
||||
}
|
||||
|
||||
// ensure the output matches expected
|
||||
f, err := os.Open(existingConfigPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open merged kubeconfig: %v", err)
|
||||
}
|
||||
contents, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read merged kubeconfig: %v", err)
|
||||
}
|
||||
expected := `apiVersion: v1
|
||||
kind: Config
|
||||
preferences: {}
|
||||
`
|
||||
assert.StringEqual(t, expected, string(contents))
|
||||
}
|
||||
|
||||
func testRemoveKINDKeepOther(t *testing.T) {
|
||||
// tests removing a kind cluster but keeping another cluster
|
||||
t.Parallel()
|
||||
dir, err := ioutil.TempDir("", "kind-testremovekind")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create tempdir: %d", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// create an existing kubeconfig
|
||||
const existingConfig = `clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: definitelyacert
|
||||
server: https://192.168.9.4:6443
|
||||
name: kind-foo
|
||||
- cluster:
|
||||
certificate-authority-data: definitelyacert
|
||||
server: https://192.168.9.4:6443
|
||||
name: kops-foo
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kind-foo
|
||||
user: kind-foo
|
||||
name: kind-foo
|
||||
- context:
|
||||
cluster: kops-foo
|
||||
user: kops-foo
|
||||
name: kops-foo
|
||||
current-context: kops-foo
|
||||
kind: Config
|
||||
apiVersion: v1
|
||||
preferences: {}
|
||||
users:
|
||||
- name: kind-foo
|
||||
user:
|
||||
client-certificate-data: seemslegit
|
||||
client-key-data: yep
|
||||
- name: kops-foo
|
||||
user:
|
||||
client-certificate-data: seemslegit
|
||||
client-key-data: yep
|
||||
`
|
||||
existingConfigPath := filepath.Join(dir, "existing-kubeconfig")
|
||||
if err := ioutil.WriteFile(existingConfigPath, []byte(existingConfig), os.ModePerm); err != nil {
|
||||
t.Fatalf("Failed to create existing kubeconfig: %d", err)
|
||||
}
|
||||
|
||||
// ensure that we can write this merged config
|
||||
if err := RemoveKIND("foo", existingConfigPath); err != nil {
|
||||
t.Fatalf("Failed to remove kind from kubeconfig: %v", err)
|
||||
}
|
||||
|
||||
// ensure the output matches expected
|
||||
f, err := os.Open(existingConfigPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open merged kubeconfig: %v", err)
|
||||
}
|
||||
contents, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read merged kubeconfig: %v", err)
|
||||
}
|
||||
expected := `apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: definitelyacert
|
||||
server: https://192.168.9.4:6443
|
||||
name: kops-foo
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kops-foo
|
||||
user: kops-foo
|
||||
name: kops-foo
|
||||
current-context: kops-foo
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: kops-foo
|
||||
user:
|
||||
client-certificate-data: seemslegit
|
||||
client-key-data: yep
|
||||
`
|
||||
assert.StringEqual(t, expected, string(contents))
|
||||
}
|
||||
89
pkg/internal/util/kubeconfig/types.go
Normal file
89
pkg/internal/util/kubeconfig/types.go
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
/*
|
||||
NOTE: all of these types are based on the upstream v1 types from client-go
|
||||
https://github.com/kubernetes/client-go/blob/0bdba2f9188006fc64057c2f6d82a0f9ee0ee422/tools/clientcmd/api/v1/types.go
|
||||
|
||||
We've forked them to:
|
||||
- remove types and fields kind does not need to inspect / modify
|
||||
- generically support fields kind doesn't inspect / modify using yaml.v3
|
||||
- have clearer names (AuthInfo -> User)
|
||||
*/
|
||||
|
||||
// Config represents a KUBECONFIG, with the fields kind is likely to use
|
||||
// Other fields are handled as unstructured data purely read for writing back
|
||||
// to disk via the OtherFields field
|
||||
type Config struct {
|
||||
// Clusters is a map of referencable names to cluster configs
|
||||
Clusters []NamedCluster `yaml:"clusters,omitempty"`
|
||||
// Users is a map of referencable names to user configs
|
||||
Users []NamedUser `yaml:"users,omitempty"`
|
||||
// Contexts is a map of referencable names to context configs
|
||||
Contexts []NamedContext `yaml:"contexts,omitempty"`
|
||||
// CurrentContext is the name of the context that you would like to use by default
|
||||
CurrentContext string `yaml:"current-context,omitempty"`
|
||||
// OtherFields contains fields kind does not inspect or modify, these are
|
||||
// read purely for writing back
|
||||
OtherFields map[string]interface{} `yaml:",inline,omitempty"`
|
||||
}
|
||||
|
||||
// NamedCluster relates nicknames to cluster information
|
||||
type NamedCluster struct {
|
||||
// Name is the nickname for this Cluster
|
||||
Name string `yaml:"name"`
|
||||
// Cluster holds the cluster information
|
||||
Cluster Cluster `yaml:"cluster"`
|
||||
}
|
||||
|
||||
// Cluster contains information about how to communicate with a kubernetes cluster
|
||||
type Cluster struct {
|
||||
// Server is the address of the kubernetes cluster (https://hostname:port).
|
||||
Server string `yaml:"server,omitempty"`
|
||||
// OtherFields contains fields kind does not inspect or modify, these are
|
||||
// read purely for writing back
|
||||
OtherFields map[string]interface{} `yaml:",inline,omitempty"`
|
||||
}
|
||||
|
||||
// NamedUser relates nicknames to user information
|
||||
type NamedUser struct {
|
||||
// Name is the nickname for this User
|
||||
Name string `yaml:"name"`
|
||||
// User holds the user information
|
||||
// We do not touch this and merely write it back
|
||||
User map[string]interface{} `yaml:"user"`
|
||||
}
|
||||
|
||||
// NamedContext relates nicknames to context information
|
||||
type NamedContext struct {
|
||||
// Name is the nickname for this Context
|
||||
Name string `yaml:"name"`
|
||||
// Context holds the context information
|
||||
Context Context `yaml:"context"`
|
||||
}
|
||||
|
||||
// Context is a tuple of references to a cluster (how do I communicate with a kubernetes cluster), a user (how do I identify myself), and a namespace (what subset of resources do I want to work with)
|
||||
type Context struct {
|
||||
// Cluster is the name of the cluster for this context
|
||||
Cluster string `yaml:"cluster"`
|
||||
// User is the name of the User for this context
|
||||
User string `yaml:"user"`
|
||||
// OtherFields contains fields kind does not inspect or modify, these are
|
||||
// read purely for writing back
|
||||
OtherFields map[string]interface{} `yaml:",inline,omitempty"`
|
||||
}
|
||||
45
pkg/internal/util/kubeconfig/write.go
Normal file
45
pkg/internal/util/kubeconfig/write.go
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes 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
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"sigs.k8s.io/kind/pkg/errors"
|
||||
)
|
||||
|
||||
// write writes cfg to configPath
|
||||
// it will ensure the directories in the path if necessary
|
||||
func write(cfg *Config, configPath string) error {
|
||||
encoded, err := Encode(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// NOTE: 0755 / 0600 are to match client-go
|
||||
dir := filepath.Dir(configPath)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||
return errors.Wrap(err, "failed to create directory for KUBECONFIG")
|
||||
}
|
||||
}
|
||||
if err := ioutil.WriteFile(configPath, encoded, 0600); err != nil {
|
||||
return errors.Wrap(err, "failed to write KUBECONFIG")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user