Repository: loft-sh/vcluster (Go)

Issue:
vcluster panic when connecting using --kube-config-context-name
### What happened?

Trying to connect to a vcluster.

the following code works

```
vcluster \
    --context   "${KUBE_CLU_NAME}" \
    --namespace "${VCLU_NS}" \
    connect "${VCLU_NAME}" \
    --server https://${VCLU_NAME}.${VCLU_DNS_SUFFIX} \
```

but if we add --kube-config-context-name it fails

```
vcluster \
    --context   "${KUBE_CLU_NAME}" \
    --namespace "${VCLU_NS}" \
    connect "${VCLU_NAME}" \
    --server https://${VCLU_NAME}.${VCLU_DNS_SUFFIX} \
    --kube-config-context-name "${VCLU_NAME}"
```

Please note that this fails even if we didn't run the 1st command before

```
vcluster --context eks-cluster-01 --namespace v-clu-1-name connect v-clu-1-name --server https://v-clu-1-name.somedomain.someext --kube-config-context-name v-clu-1-name
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x103c3101c]

goroutine 1 [running]:
k8s.io/client-go/tools/clientcmd.ModifyConfig({0x104825080, 0x1400011a9b0}, {{0x0, 0x0}, {0x0, 0x0}, {0x0, 0x1400015ff80}, 0x1400015ffb0, 0x140005420c0, ...}, ...)
        k8s.io/client-go@v0.26.1/tools/clientcmd/config.go:208 +0x38c
github.com/loft-sh/vcluster/cmd/vclusterctl/cmd.updateKubeConfig({0x16d2bf719, 0x14}, 0x0, 0x0, 0x1)
        github.com/loft-sh/vcluster/cmd/vclusterctl/cmd/util.go:143 +0x328
github.com/loft-sh/vcluster/cmd/vclusterctl/cmd.(*ConnectCmd).Connect(0x140005afe00, {0x16d2bf6a6, 0x14}, {0x1400014ba80, 0x0, 0x8})
        github.com/loft-sh/vcluster/cmd/vclusterctl/cmd/connect.go:181 +0x784
github.com/loft-sh/vcluster/cmd/vclusterctl/cmd.(*ConnectCmd).Run(...)
        github.com/loft-sh/vcluster/cmd/vclusterctl/cmd/connect.go:128
github.com/loft-sh/vcluster/cmd/vclusterctl/cmd.NewConnectCmd.func1(0x14000622c00?, {0x1400014ba70, 0x1, 0x9})
        github.com/loft-sh/vcluster/cmd/vclusterctl/cmd/connect.go:101 +0x88
github.com/spf13/cobra.(*Command).execute(0x14000622c00, {0x1400014b9e0, 0x9, 0x9})
        github.com/spf13/cobra@v1.6.0/command.go:916 +0x5c8
github.com/spf13/cobra.(*Command).ExecuteC(0x14000622900)
        github.com/spf13/cobra@v1.6.0/command.go:1040 +0x354
github.com/spf13/cobra.(*Command).Execute(...)
        github.com/spf13/cobra@v1.6.0/command.go:968
github.com/loft-sh/vcluster/cmd/vclusterctl/cmd.Execute()
        github.com/loft-sh/vcluster/cmd/vclusterctl/cmd/root.go:47 +0xbc
main.main()
        ./main.go:17 +0x30
```

### What did you expect to happen?

the command to work, and add an entry to \~/.kube/config with the designated context

### How can we reproduce it (as minimally and precisely as possible)?

(see description above)

### Anything else we need to know?

* CLI: vcluster 0.15.0 on macOS ventura (arm m1)
* Server: EKS 1.26
* VCluster: with vcluster-eks 0.15.0 deployed via helm chart (from flux2)

### Host cluster Kubernetes version

<details>

```
$ kubectl version
WARNING: This version information is deprecated and will be replaced with the output from kubectl version --short.  Use --output=yaml|json to get the full version.
Client Version: version.Info{Major:"1", Minor:"27", GitVersion:"v1.27.1", GitCommit:"4c9411232e10168d7b050c49a1b59f6df9d7ea4b", GitTreeState:"clean", BuildDate:"2023-04-14T13:14:41Z", GoVersion:"go1.20.3", Compiler:"gc", Platform:"darwin/arm64"}
Kustomize Version: v5.0.1
Server Version: version.Info{Major:"1", Minor:"26+", GitVersion:"v1.26.2-eks-a59e1f0", GitCommit:"8b68f4b95d7121d039ceebd30870e48acc7772e4", GitTreeState:"clean", BuildDate:"2023-03-09T19:59:45Z", GoVersion:"go1.19.6", Compiler:"gc", Platform:"linux/amd64"}
```

</details>

### Host cluster Kubernetes distribution

<details>

```
EKS 1.26
```

</details>

### vlcuster version

<details>

```
$ vcluster --version
vcluster version 0.15.0
```

</details>

### Vcluster Kubernetes distribution(k3s(default)), k8s, k0s)

<details>

```
eks
```

</details>

### OS and Arch

<details>

```
OS: linux
Arch: amd64
```

</details>


Discussion:
Thanks for the report, will fix :)
Hmm, tried a lot to repro with ingress on minikube., but couldn't reproduce. The nil dereference seems to be in clientcmd:
```
			destinationFile := cluster.LocationOfOrigin
```

Perhaps it's triggered by some form of quirk in eks kubeconfigs? I'll investigate how this is used.
Ah, it's possible the `cluster` value itself is somehow nil.

Edit: It is, I should have looked at the stacktrace closer: `github.com/loft-sh/vcluster/cmd/vclusterctl/cmd.updateKubeConfig({0x16d2bf719, 0x14}, 0x0, 0x0, 0x1)`
function signature: `func updateKubeConfig(contextName string, cluster *api.Cluster, authInfo *api.AuthInfo, setActive bool) error`
Recieved args:
- contextName: `{0x16d2bf719, 0x14}` (Some str with len 20)
- cluster: `nil`
- authInfo: `nil`
- setActive: `true`

@joaocc, since I'm having trouble reproducing the issue can you share:
- does this happen on fresh clusters or is this limited to a particular cluster?
- Any extra values you might have set for the helm chart? 
- The output of `k get secret -n ${VCLUSTER_NAMESPACE} ${VCLUSTER_NAME}-v1 -o jsonpath='{.data.config}'|base64 -d`  please redact the cert authority data and obfuscate any addresses, etc. as this is the kubeconfig of your cluster.
Hi
It happened with several vclusters that were freshly installed.
Install was with the following options:
```yaml
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: my-vcluster-dns
  namespace: my-vcluster-dns
spec:
  chart:
    spec:
      chart: vcluster-eks
      version: "0.15.0" # ">=1.0.0"
      sourceRef:
        kind: HelmRepository
        name: loft-sh.vcluster
        namespace: flux-system
  interval: 5m
  install:
    remediation:
      retries: 3
  values:
    sync:
      services:
        enabled: true # already set by default, but reinforcing...
        secrets:
          enabled: true
          all: true
        persistentvolumeclaims:
          enabled: true
      serviceaccounts:
        enabled: true
    proxy:
      metricsServer:
        nodes:
          enabled: true
        pods:
          enabled: true
    syncer:
      extraArgs: # []
      - --tls-san=my-vcluster-dns.something.local
      - --out-kube-config-server=https://my-vcluster-dns.something.local
      kubeConfigContextName: "my-vcluster-dns"
    etcd:
      storage:
        persistence: true
        size: 10Gi
        className: some-storageclass
    service:
      type: ClusterIP
    ingress:
      enabled: true
      annotations:
        nginx.ingress.kubernetes.io/backend-protocol: HTTPS
        nginx.ingress.kubernetes.io/ssl-passthrough: "true"
        nginx.ingress.kubernetes.io/ssl-redirect: "true"
      ingressClassName: "some-ingress"
      host: my-vcluster-dns.something.local
    isolation:
      enabled: false
    telemetry:
      disabled: "true"

```
Regarding the request for the secret, we don't have any with that name.
the closest we have is
*  ${VCLUSTER_NAME}-certs
```yaml
apiVersion: v1
kind: Secret
metadata:
  name: vc-my-vcluster-dns
  namespace: my-vcluster-dns
type: Opaque
stringData:
  certificate-authority: >-
    -----BEGIN CERTIFICATE-----
    ...
    -----END CERTIFICATE-----

  client-certificate: >-
    -----BEGIN CERTIFICATE-----
    ...
    -----END CERTIFICATE-----
  client-key: >-
    -----BEGIN RSA PRIVATE KEY-----
    ...
    -----END RSA PRIVATE KEY-----

  config: >-
    apiVersion: v1
    clusters:
    - cluster:
        certificate-authority-data: ...
        server: https://my-vcluster-dns.something.local
      name: my-vcluster-dns
    contexts:
    - context:
        cluster: my-vcluster-dns
        user: my-vcluster-dns
      name: my-vcluster-dns
    current-context: my-vcluster-dns
    kind: Config
    preferences: {}
    users:
    - name: my-vcluster-dns
      user:
        client-certificate-data: ...
        client-key-data: ...
```
* vc-${VCLUSTER_NAME}
with other content

Anyway, we reinstalled them with etcd (due to incompat of SQLite in k3s with NFS) so I will give it another go and let you know
Thanks :) I'll send a PR with some fixes that should remove that panic just error more insightfully, but I still can't seem to reproduce the issue. Doesn't seem to be a quirk with your vluster version though. What's your vclusterctl version btw ?(assuming that's also 0.15.0, but better to rule that out)
Thx.
yes, it is v0.15.0, both in cli and in vcluster.
Turns out it was only triggered if the --kube-config-context-name was the same as the one the syncer used! A mistake in rewriting it. Thanks for taking the time to debug :)
Thx. May I also ask what would be the practical effect of specifying kube-config-context-name? I didn't get it from the documentation (and tried it just out of intuition). 
When modifying your local kubeconfig file, it simply sets the name of the context to whatever is the value of that argument. It does have limited practical use, but I'll look into the existing docs/help to see if there's ambiguity
I think it is quite useful. When adding them to EKS, the resulting vcluster name is very very long. It helps in getting a friendlier name, without having to hack kubeconfig file.



Patch:
diff --git a/cmd/vclusterctl/cmd/connect.go b/cmd/vclusterctl/cmd/connect.go
index 36266952db..2876705823 100644
--- a/cmd/vclusterctl/cmd/connect.go
+++ b/cmd/vclusterctl/cmd/connect.go
@@ -500,6 +500,9 @@ func (cmd *ConnectCmd) setServerIfExposed(vClusterName string, vClusterConfig *a
 	return nil
 }
 
+// exchangeContextName switches the context name specified in the remote kubeconfig with
+// the context name specified by the user. It cannot correctly handle kubeconfigs with multiple entries
+// for clusters, authInfos, contexts, but ideally this is pointed at a secret created by us.
 func (cmd *ConnectCmd) exchangeContextName(kubeConfig *api.Config, vclusterName string) error {
 	if cmd.KubeConfigContextName == "" {
 		if vclusterName != "" {
@@ -509,27 +512,36 @@ func (cmd *ConnectCmd) exchangeContextName(kubeConfig *api.Config, vclusterName
 		}
 	}
 
-	// update cluster
+	// pick the last specified cluster (there should ideally be exactly one)
 	for k := range kubeConfig.Clusters {
 		kubeConfig.Clusters[cmd.KubeConfigContextName] = kubeConfig.Clusters[k]
-		delete(kubeConfig.Clusters, k)
+		// delete the rest
+		if k != cmd.KubeConfigContextName {
+			delete(kubeConfig.Clusters, k)
+		}
 		break
 	}
 
-	// update context
+	// pick the last specified context (there should ideally be exactly one)
 	for k := range kubeConfig.Contexts {
 		ctx := kubeConfig.Contexts[k]
 		ctx.Cluster = cmd.KubeConfigContextName
 		ctx.AuthInfo = cmd.KubeConfigContextName
 		kubeConfig.Contexts[cmd.KubeConfigContextName] = ctx
-		delete(kubeConfig.Contexts, k)
+		// delete the rest
+		if k != cmd.KubeConfigContextName {
+			delete(kubeConfig.Contexts, k)
+		}
 		break
 	}
 
-	// update authInfo
+	// pick the last specified authinfo (there should ideally be exactly one)
 	for k := range kubeConfig.AuthInfos {
 		kubeConfig.AuthInfos[cmd.KubeConfigContextName] = kubeConfig.AuthInfos[k]
-		delete(kubeConfig.AuthInfos, k)
+		// delete the rest
+		if k != cmd.KubeConfigContextName {
+			delete(kubeConfig.AuthInfos, k)
+		}
 		break
 	}
 


Test patch:
diff --git a/cmd/vclusterctl/cmd/connect_test.go b/cmd/vclusterctl/cmd/connect_test.go
new file mode 100644
index 0000000000..4ae5aff4c4
--- /dev/null
+++ b/cmd/vclusterctl/cmd/connect_test.go
@@ -0,0 +1,98 @@
+package cmd
+
+import (
+	"testing"
+
+	"github.com/loft-sh/vcluster/cmd/vclusterctl/flags"
+	"gotest.tools/v3/assert"
+	"k8s.io/client-go/tools/clientcmd/api"
+)
+
+const (
+	defaultContextName = "my-vcluster"
+)
+
+func TestExchangeContextName(t *testing.T) {
+	vclusterName := "vcluster-name"
+	namespace := "default"
+	defaultContext := &api.Config{
+		Clusters: map[string]*api.Cluster{
+			defaultContextName: {Server: "foo"},
+		},
+		AuthInfos: map[string]*api.AuthInfo{
+			defaultContextName: {
+				Token: "foo",
+			},
+		},
+		Contexts: map[string]*api.Context{
+			defaultContextName: {
+				Cluster:  defaultContextName,
+				AuthInfo: defaultContextName,
+			},
+		},
+		CurrentContext: defaultContextName,
+	}
+	testTable := []struct {
+		desc           string
+		newContextName string
+		config         *api.Config
+		expectedConfig *api.Config
+	}{
+		{
+			desc:           "KubeConfigContextName specified",
+			newContextName: "new-context",
+			config:         defaultContext.DeepCopy(),
+			expectedConfig: &api.Config{
+				Clusters: map[string]*api.Cluster{
+					"new-context": {Server: "foo"},
+				},
+				AuthInfos: map[string]*api.AuthInfo{
+					"new-context": {
+						Token: "foo",
+					},
+				},
+				Contexts: map[string]*api.Context{
+					"new-context": {
+						Cluster:  "new-context",
+						AuthInfo: "new-context",
+					},
+				},
+				CurrentContext: "new-context",
+			},
+		},
+		{
+			desc:           "KubeConfigContextName same as default",
+			newContextName: defaultContextName,
+			config:         defaultContext.DeepCopy(),
+			expectedConfig: &api.Config{
+				Clusters: map[string]*api.Cluster{
+					defaultContextName: &api.Cluster{Server: "foo"},
+				},
+				AuthInfos: map[string]*api.AuthInfo{
+					defaultContextName: &api.AuthInfo{
+						Token: "foo",
+					},
+				},
+				Contexts: map[string]*api.Context{
+					defaultContextName: &api.Context{
+						Cluster:  defaultContextName,
+						AuthInfo: defaultContextName,
+					},
+				},
+				CurrentContext: defaultContextName,
+			},
+		},
+	}
+	for _, testCase := range testTable {
+		cmd := &ConnectCmd{
+			GlobalFlags: &flags.GlobalFlags{
+				Namespace: namespace,
+			},
+			KubeConfigContextName: testCase.newContextName,
+		}
+		assert.NilError(t, cmd.exchangeContextName(testCase.config, vclusterName))
+		newConfig := testCase.config.DeepCopy()
+
+		assert.DeepEqual(t, newConfig, testCase.expectedConfig)
+	}
+}