Skip to content

Commit

Permalink
Store kubeconfig in ToolchainCluster secret (#404)
Browse files Browse the repository at this point in the history
Introduce a temporary migration step that converts the connection details
spread across the ToolchainCluster and its associated secret into the
"kubeconfig" field in the secret data that contains the serialized kubeconfig
file.
  • Loading branch information
metlos authored Jun 27, 2024
1 parent d3f78a7 commit 7ec2869
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 0 deletions.
67 changes: 67 additions & 0 deletions controllers/toolchaincluster/toolchaincluster_controller.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package toolchaincluster

import (
"bytes"
"context"
"fmt"
"time"

toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
"github.com/codeready-toolchain/toolchain-common/pkg/cluster"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
kubeclientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
Expand Down Expand Up @@ -61,6 +65,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.
return reconcile.Result{}, err
}

if err = r.migrateSecretToKubeConfig(ctx, toolchainCluster); err != nil {
return reconcile.Result{}, err
}

clientSet, err := kubeclientset.NewForConfig(cachedCluster.RestConfig)
if err != nil {
reqLogger.Error(err, "cannot create ClientSet for the ToolchainCluster")
Expand All @@ -80,3 +88,62 @@ func (r *Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.

return reconcile.Result{RequeueAfter: r.RequeAfter}, nil
}

func (r *Reconciler) migrateSecretToKubeConfig(ctx context.Context, tc *toolchainv1alpha1.ToolchainCluster) error {
if len(tc.Spec.SecretRef.Name) == 0 {
return nil
}

secret := &corev1.Secret{}
if err := r.Client.Get(ctx, client.ObjectKey{Name: tc.Spec.SecretRef.Name, Namespace: tc.Namespace}, secret); err != nil {
return err
}

token := secret.Data["token"]
apiEndpoint := tc.Spec.APIEndpoint
operatorNamespace := tc.Labels["namespace"]
insecureTls := len(tc.Spec.DisabledTLSValidations) == 1 && tc.Spec.DisabledTLSValidations[0] == "*"
// we ignore the Spec.CABundle here because we don't want it migrated. The new configurations are free
// to use the certificate data for the connections but we don't want to migrate the existing certificates.
kubeConfig := composeKubeConfigFromData(token, apiEndpoint, operatorNamespace, insecureTls)

data, err := clientcmd.Write(kubeConfig)
if err != nil {
return err
}

origKubeConfigData := secret.Data["kubeconfig"]
secret.Data["kubeconfig"] = data

if !bytes.Equal(origKubeConfigData, data) {
if err = r.Client.Update(ctx, secret); err != nil {
return err
}
}

return nil
}

func composeKubeConfigFromData(token []byte, apiEndpoint, operatorNamespace string, insecureTls bool) clientcmdapi.Config {
return clientcmdapi.Config{
Contexts: map[string]*clientcmdapi.Context{
"ctx": {
Cluster: "cluster",
Namespace: operatorNamespace,
AuthInfo: "auth",
},
},
CurrentContext: "ctx",
Clusters: map[string]*clientcmdapi.Cluster{
"cluster": {
Server: apiEndpoint,
InsecureSkipTLSVerify: insecureTls,
},
},
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"auth": {
Token: string(token),
},
},
}
}
52 changes: 52 additions & 0 deletions controllers/toolchaincluster/toolchaincluster_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ import (
toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
"github.com/codeready-toolchain/toolchain-common/pkg/cluster"
"github.com/codeready-toolchain/toolchain-common/pkg/test"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/h2non/gock.v1"
"gotest.tools/assert/cmp"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"sigs.k8s.io/controller-runtime/pkg/client"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
Expand Down Expand Up @@ -119,6 +124,53 @@ func TestClusterControllerChecks(t *testing.T) {
require.NoError(t, err)
assertClusterStatus(t, cl, "stable", offline())
})

t.Run("migrates connection settings to kubeconfig in secret", func(t *testing.T) {
// given
tc, secret := newToolchainCluster("tc", tcNs, "http://cluster.com", toolchainv1alpha1.ToolchainClusterStatus{})
cl := test.NewFakeClient(t, tc, secret)
reset := setupCachedClusters(t, cl, tc)
defer reset()

controller, req := prepareReconcile(tc, cl, requeAfter)
expectedKubeConfig := composeKubeConfigFromData([]byte("mycooltoken"), "http://cluster.com", "test-namespace", true)

// when
_, err := controller.Reconcile(context.TODO(), req)
secretAfterReconcile := &corev1.Secret{}
require.NoError(t, cl.Get(context.TODO(), client.ObjectKeyFromObject(secret), secretAfterReconcile))
actualKubeConfig, loadErr := clientcmd.Load(secretAfterReconcile.Data["kubeconfig"])

// then
require.NoError(t, err)
require.NoError(t, loadErr)
assert.Contains(t, secretAfterReconcile.Data, "kubeconfig")

// we need to use this more complex equals, because we don't initialize the Extension fields (i.e. they're nil)
// while they're initialized and empty after deserialization, which causes the "normal" deep equals to fail.
result := cmp.DeepEqual(expectedKubeConfig, *actualKubeConfig,
cmpopts.IgnoreFields(clientcmdapi.Config{}, "Extensions"),
cmpopts.IgnoreFields(clientcmdapi.Preferences{}, "Extensions"),
cmpopts.IgnoreFields(clientcmdapi.Cluster{}, "Extensions"),
cmpopts.IgnoreFields(clientcmdapi.AuthInfo{}, "Extensions"),
cmpopts.IgnoreFields(clientcmdapi.Context{}, "Extensions"),
)()

assert.True(t, result.Success())
})
}

func TestComposeKubeConfig(t *testing.T) {
// when
kubeConfig := composeKubeConfigFromData([]byte("token"), "http://over.the.rainbow", "the-namespace", false)

// then
context := kubeConfig.Contexts[kubeConfig.CurrentContext]

assert.Equal(t, "token", kubeConfig.AuthInfos[context.AuthInfo].Token)
assert.Equal(t, "http://over.the.rainbow", kubeConfig.Clusters[context.Cluster].Server)
assert.Equal(t, "the-namespace", context.Namespace)
assert.False(t, kubeConfig.Clusters[context.Cluster].InsecureSkipTLSVerify)
}

func setupCachedClusters(t *testing.T, cl *test.FakeClient, clusters ...*toolchainv1alpha1.ToolchainCluster) func() {
Expand Down

0 comments on commit 7ec2869

Please sign in to comment.