Merge pull request #1029 from BenTheElder/kubeconfig

kubeconfig overhaul
This commit is contained in:
Kubernetes Prow Robot
2019-10-30 23:33:36 -07:00
committed by GitHub
28 changed files with 1830 additions and 266 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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,
)
}

View File

@@ -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

View File

@@ -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
}

View 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)
}

View 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
}

View 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))
}

View 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
}

View 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))
})
}
}

View File

@@ -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 := ""

View 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"
}

View 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
}

View 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))
}

View 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]
}

View 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
}

View 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"]))
}
})
}

View 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
}

View 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))
}

View 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"`
}

View 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
}