External datastore support (#879)

* Adding new k3s function to get the config
* Adding a new way to get the bootstrap data directly from the server Pod
* Gate the bootstrap fetch if external datastore is in use
---------

Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com>
This commit is contained in:
Hussein Galal
2026-06-26 17:22:00 +03:00
committed by GitHub
parent 244011e68d
commit 3abffae92d
18 changed files with 489 additions and 53 deletions
@@ -1413,6 +1413,15 @@ spec:
MountPath is the path within server and agent pods where the
secret contents will be mounted.
type: string
name:
description: |-
Name is the name of the secret mount volume that will be used
as the name of volume and volume mount for the server or agent pod
if empty then the secret name will be used instead.
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
optional:
description: optional field specify whether the Secret or its keys must be defined
type: boolean
+6
View File
@@ -612,6 +612,12 @@ Directories within the path are not affected by this setting. +
This might be in conflict with other options that affect the file +
mode, like fsGroup, and the result can be other mode bits set. + | |
| *`optional`* __boolean__ | optional field specify whether the Secret or its keys must be defined + | |
| *`name`* __string__ | Name is the name of the secret mount volume that will be used +
as the name of volume and volume mount for the server or agent pod +
if empty then the secret name will be used instead. + | | MaxLength: 63 +
MinLength: 1 +
Pattern: `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` +
| *`mountPath`* __string__ | MountPath is the path within server and agent pods where the +
secret contents will be mounted. + | |
| *`subPath`* __string__ | SubPath is an optional path within the secret to mount instead of the root. +
+1
View File
@@ -452,6 +452,7 @@ _Appears in:_
| `items` _[KeyToPath](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#keytopath-v1-core) array_ | items If unspecified, each key-value pair in the Data field of the referenced<br />Secret will be projected into the volume as a file whose name is the<br />key and content is the value. If specified, the listed keys will be<br />projected into the specified paths, and unlisted keys will not be<br />present. If a key is specified which is not present in the Secret,<br />the volume setup will error unless it is marked optional. Paths must be<br />relative and may not contain the '..' path or start with '..'. | | |
| `defaultMode` _integer_ | defaultMode is Optional: mode bits used to set permissions on created files by default.<br />Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511.<br />YAML accepts both octal and decimal values, JSON requires decimal values<br />for mode bits. Defaults to 0644.<br />Directories within the path are not affected by this setting.<br />This might be in conflict with other options that affect the file<br />mode, like fsGroup, and the result can be other mode bits set. | | |
| `optional` _boolean_ | optional field specify whether the Secret or its keys must be defined | | |
| `name` _string_ | Name is the name of the secret mount volume that will be used<br />as the name of volume and volume mount for the server or agent pod<br />if empty then the secret name will be used instead. | | MaxLength: 63 <br />MinLength: 1 <br />Pattern: `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` <br /> |
| `mountPath` _string_ | MountPath is the path within server and agent pods where the<br />secret contents will be mounted. | | |
| `subPath` _string_ | SubPath is an optional path within the secret to mount instead of the root.<br />When specified, only the specified key from the secret will be mounted as a file<br />at MountPath, keeping the parent directory writable. | | |
| `role` _string_ | Role is the type of the k3k pod that will be used to mount the secret.<br />This can be 'server', 'agent', or 'all' (for both). | | Enum: [server agent all] <br /> |
+1 -1
View File
@@ -78,7 +78,7 @@ func loadTLSConfig(cfg config, token, agentIP, podIP string) (*tls.Config, error
}, func() error {
var err error
tlsCrt, err = k3s.GetServingKubeletCrt(client)
tlsCrt, err = client.GetServingKubeletCrt()
return err
}); err != nil {
+9
View File
@@ -241,6 +241,15 @@ type ClusterSpec struct {
type SecretMount struct {
// Embeds SecretName, Items, DefaultMode, and Optional
corev1.SecretVolumeSource `json:",inline"`
// Name is the name of the secret mount volume that will be used
// as the name of volume and volume mount for the server or agent pod
// if empty then the secret name will be used instead.
//
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
// +optional
Name string `json:"name,omitempty"`
// MountPath is the path within server and agent pods where the
// secret contents will be mounted.
//
+2 -2
View File
@@ -29,11 +29,11 @@ type Config struct {
scheme *runtime.Scheme
}
func NewConfig(cluster *v1beta1.Cluster, client ctrlruntimeclient.Client, scheme *runtime.Scheme) *Config {
func NewConfig(cluster *v1beta1.Cluster, client ctrlruntimeclient.Client) *Config {
return &Config{
cluster: cluster,
client: client,
scheme: scheme,
scheme: client.Scheme(),
}
}
+19 -14
View File
@@ -12,9 +12,9 @@ import (
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/discovery"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
@@ -87,7 +87,7 @@ type Config struct {
type ClusterReconciler struct {
DiscoveryClient *discovery.DiscoveryClient
Client client.Client
Scheme *runtime.Scheme
RestCfg *rest.Config
PortAllocator *agent.PortAllocator
record.EventRecorder
@@ -113,7 +113,7 @@ func Add(ctx context.Context, mgr manager.Manager, config *Config, maxConcurrent
reconciler := ClusterReconciler{
DiscoveryClient: discoveryClient,
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
RestCfg: mgr.GetConfig(),
EventRecorder: eventRecorder,
PortAllocator: portAllocator,
Config: Config{
@@ -462,12 +462,17 @@ func (c *ClusterReconciler) ensureBootstrapSecret(ctx context.Context, cluster *
log := ctrl.LoggerFrom(ctx)
log.V(1).Info("Ensuring bootstrap secret")
data, err := bootstrap.Fetch(ctx, serviceIP, token)
k3sClient := k3s.New(k3s.ClientConfig{
ServerIP: serviceIP,
Token: token,
})
data, err := bootstrap.Fetch(ctx, k3sClient, cluster, c.RestCfg)
if err != nil {
return err
}
return bootstrap.SaveToSecret(ctx, c.Client, c.Scheme, cluster, data)
return bootstrap.SaveToSecret(ctx, c.Client, cluster, data)
}
// ensureKubeconfigSecret will create or update the Secret containing the kubeconfig data from the k3s server
@@ -495,7 +500,7 @@ func (c *ClusterReconciler) ensureKubeconfigSecret(ctx context.Context, cluster
}
_, err = controllerutil.CreateOrUpdate(ctx, c.Client, kubeconfigSecret, func() error {
if err := controllerutil.SetControllerReference(cluster, kubeconfigSecret, c.Scheme); err != nil {
if err := controllerutil.SetControllerReference(cluster, kubeconfigSecret, c.Client.Scheme()); err != nil {
return err
}
@@ -516,7 +521,7 @@ func (c *ClusterReconciler) createClusterConfigs(ctx context.Context, cluster *v
return err
}
if err := controllerutil.SetControllerReference(cluster, initServerConfig, c.Scheme); err != nil {
if err := controllerutil.SetControllerReference(cluster, initServerConfig, c.Client.Scheme()); err != nil {
return err
}
@@ -532,7 +537,7 @@ func (c *ClusterReconciler) createClusterConfigs(ctx context.Context, cluster *v
return err
}
if err := controllerutil.SetControllerReference(cluster, serverConfig, c.Scheme); err != nil {
if err := controllerutil.SetControllerReference(cluster, serverConfig, c.Client.Scheme()); err != nil {
return err
}
@@ -625,7 +630,7 @@ func (c *ClusterReconciler) ensureNetworkPolicy(ctx context.Context, cluster *v1
currentNetworkPolicy := expectedNetworkPolicy.DeepCopy()
result, err := controllerutil.CreateOrUpdate(ctx, c.Client, currentNetworkPolicy, func() error {
if err := controllerutil.SetControllerReference(cluster, currentNetworkPolicy, c.Scheme); err != nil {
if err := controllerutil.SetControllerReference(cluster, currentNetworkPolicy, c.Client.Scheme()); err != nil {
return err
}
@@ -653,7 +658,7 @@ func (c *ClusterReconciler) ensureClusterService(ctx context.Context, cluster *v
currentService := expectedService.DeepCopy()
result, err := controllerutil.CreateOrUpdate(ctx, c.Client, currentService, func() error {
if err := controllerutil.SetControllerReference(cluster, currentService, c.Scheme); err != nil {
if err := controllerutil.SetControllerReference(cluster, currentService, c.Client.Scheme()); err != nil {
return err
}
@@ -688,7 +693,7 @@ func (c *ClusterReconciler) ensureIngress(ctx context.Context, cluster *v1beta1.
currentServerIngress := expectedServerIngress.DeepCopy()
result, err := controllerutil.CreateOrUpdate(ctx, c.Client, currentServerIngress, func() error {
if err := controllerutil.SetControllerReference(cluster, currentServerIngress, c.Scheme); err != nil {
if err := controllerutil.SetControllerReference(cluster, currentServerIngress, c.Client.Scheme()); err != nil {
return err
}
@@ -828,7 +833,7 @@ func (c *ClusterReconciler) server(ctx context.Context, cluster *v1beta1.Cluster
// create headless service for the statefulset
serverStatefulService := server.StatefulServerService()
if err := controllerutil.SetControllerReference(cluster, serverStatefulService, c.Scheme); err != nil {
if err := controllerutil.SetControllerReference(cluster, serverStatefulService, c.Client.Scheme()); err != nil {
return err
}
@@ -848,7 +853,7 @@ func (c *ClusterReconciler) server(ctx context.Context, cluster *v1beta1.Cluster
currentServerStatefulSet := expectedServerStatefulSet.DeepCopy()
result, err := controllerutil.CreateOrUpdate(ctx, c.Client, currentServerStatefulSet, func() error {
if err := controllerutil.SetControllerReference(cluster, currentServerStatefulSet, c.Scheme); err != nil {
if err := controllerutil.SetControllerReference(cluster, currentServerStatefulSet, c.Client.Scheme()); err != nil {
return err
}
@@ -896,7 +901,7 @@ func (c *ClusterReconciler) bindClusterRoles(ctx context.Context, cluster *v1bet
}
func (c *ClusterReconciler) ensureAgent(ctx context.Context, cluster *v1beta1.Cluster, serviceIP, token string) error {
config := agent.NewConfig(cluster, c.Client, c.Scheme)
config := agent.NewConfig(cluster, c.Client)
var agentEnsurer agent.ResourceEnsurer
if cluster.Spec.Mode == agent.VirtualNodeMode {
+7 -2
View File
@@ -41,8 +41,13 @@ func buildSecretMountVolume(secretMount v1beta1.SecretMount) (corev1.Volume, cor
},
}
volName := secretMount.Name
if volName == "" {
volName = secretMount.SecretName
}
vol := corev1.Volume{
Name: secretMount.SecretName,
Name: volName,
VolumeSource: corev1.VolumeSource{
Projected: &corev1.ProjectedVolumeSource{
Sources: projectedVolSources,
@@ -51,7 +56,7 @@ func buildSecretMountVolume(secretMount v1beta1.SecretMount) (corev1.Volume, cor
}
volMount := corev1.VolumeMount{
Name: secretMount.SecretName,
Name: volName,
MountPath: secretMount.MountPath,
SubPath: secretMount.SubPath,
}
@@ -5,8 +5,8 @@ import (
"encoding/json"
"errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -19,22 +19,29 @@ import (
"github.com/rancher/k3k/pkg/k3s"
)
const (
TLSDir = "/var/lib/rancher/k3s/server/tls/"
)
// Fetch requests bootstrap data from k3s using the token and decodes it,
// to avoid double encoding when stored as secret.
func Fetch(ctx context.Context, ip, token string) (*k3s.BootstrapData, error) {
func Fetch(ctx context.Context, k3sClient *k3s.Client, cluster *v1beta1.Cluster, restConfig *rest.Config) (*k3s.BootstrapData, error) {
log := ctrl.LoggerFrom(ctx)
log.V(1).Info("Fetching bootstrap data from K3s API")
return fetchFromK3sServer(ip, token)
config, err := k3sClient.GetServerConfig()
if err != nil {
return nil, err
}
if config.ClusterInit {
log.V(1).Info("Fetching bootstrap data from K3s server API")
return k3sClient.GetServerBootstrap()
}
log.V(1).Info("Fetching bootstrap data from K3s server Pod")
return k3s.ReadBootstrapFromK3sPod(ctx, restConfig, cluster.Name, cluster.Namespace)
}
// SaveToSecret marshals the bootstrap data and stores it in a Secret owned by the cluster,
// creating the Secret if it does not exist or updating it otherwise.
func SaveToSecret(ctx context.Context, c client.Client, scheme *runtime.Scheme, cluster *v1beta1.Cluster, data *k3s.BootstrapData) error {
func SaveToSecret(ctx context.Context, c client.Client, cluster *v1beta1.Cluster, data *k3s.BootstrapData) error {
bootstrapData, err := json.Marshal(data)
if err != nil {
return err
@@ -48,7 +55,7 @@ func SaveToSecret(ctx context.Context, c client.Client, scheme *runtime.Scheme,
}
_, err = controllerutil.CreateOrUpdate(ctx, c, secret, func() error {
if err := controllerutil.SetControllerReference(cluster, secret, scheme); err != nil {
if err := controllerutil.SetControllerReference(cluster, secret, c.Scheme()); err != nil {
return err
}
@@ -85,12 +92,3 @@ func LoadFromSecret(ctx context.Context, client client.Client, cluster *v1beta1.
return &bootstrap, err
}
func fetchFromK3sServer(serviceIP, token string) (*k3s.BootstrapData, error) {
client := k3s.New(k3s.ClientConfig{
ServerIP: serviceIP,
Token: token,
})
return k3s.GetServerBootstrap(client)
}
+1 -1
View File
@@ -131,7 +131,7 @@ configure_cgroups() {
EXTRA_ARGS="$EXTRA_ARGS --kubelet-arg=kubelet-cgroups=$root_cgroup_parent/k3s --kubelet-arg=cgroup-root=$root_cgroup_parent"
}
EXTRA_ARGS={{.EXTRA_ARGS}}
EXTRA_ARGS="{{.EXTRA_ARGS}}"
configure_cgroups
case "{{.CLUSTER_MODE}}" in
+26 -6
View File
@@ -72,12 +72,6 @@ func (p *StatefulSetReconciler) Reconcile(ctx context.Context, req reconcile.Req
return reconcile.Result{}, ctrlruntimeclient.IgnoreNotFound(err)
}
// If the StatefulSet is being deleted, we need to remove the finalizers from its pods
// and remove the finalizer from the StatefulSet itself.
if !sts.DeletionTimestamp.IsZero() {
return p.handleDeletion(ctx, &sts)
}
// get cluster name from the object
clusterKey := clusterNamespacedName(&sts)
@@ -88,6 +82,12 @@ func (p *StatefulSetReconciler) Reconcile(ctx context.Context, req reconcile.Req
}
}
// If the StatefulSet is being deleted, we need to remove the finalizers from its pods
// and remove the finalizer from the StatefulSet itself.
if !sts.DeletionTimestamp.IsZero() {
return p.handleDeletion(ctx, &sts)
}
podList, err := p.listPods(ctx, &sts)
if err != nil {
return reconcile.Result{}, err
@@ -106,6 +106,26 @@ func (p *StatefulSetReconciler) Reconcile(ctx context.Context, req reconcile.Req
}
}
// skip adding finalizer to server pods if etcd is not used
clusterToken, err := getClusterToken(ctx, p.Client, &cluster)
if err != nil {
return reconcile.Result{}, err
}
k3sClient := k3s.New(k3s.ClientConfig{
ServerIP: fmt.Sprintf("%s.%s", server.ServiceName(cluster.Name), cluster.Namespace),
Token: clusterToken,
})
config, err := k3sClient.GetServerConfig()
if err != nil {
return reconcile.Result{}, err
}
if !config.ClusterInit {
return reconcile.Result{}, nil
}
for _, pod := range podList.Items {
if err := p.handleServerPod(ctx, cluster, &pod); err != nil {
return reconcile.Result{}, err
+20 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"k8s.io/apimachinery/pkg/types"
@@ -73,7 +74,7 @@ func (c *ClusterReconciler) ensureTokenSecret(ctx context.Context, cluster *v1be
key = client.ObjectKeyFromObject(&tokenSecret)
result, err := controllerutil.CreateOrUpdate(ctx, c.Client, &tokenSecret, func() error {
return controllerutil.SetControllerReference(cluster, &tokenSecret, c.Scheme)
return controllerutil.SetControllerReference(cluster, &tokenSecret, c.Client.Scheme())
})
if result != controllerutil.OperationResultNone {
@@ -113,3 +114,21 @@ func TokenSecretObj(token, name, namespace string) corev1.Secret {
func TokenSecretName(clusterName string) string {
return controller.SafeConcatNameWithPrefix(clusterName, "token")
}
func getClusterToken(ctx context.Context, client client.Client, cluster *v1beta1.Cluster) (string, error) {
tokenSecretName := TokenSecretName(cluster.Name)
if cluster.Spec.TokenSecretRef != nil && cluster.Spec.TokenSecretRef.Name != "" {
tokenSecretName = cluster.Spec.TokenSecretRef.Name
}
var tokenSecret corev1.Secret
if err := client.Get(ctx, types.NamespacedName{Name: tokenSecretName, Namespace: cluster.Namespace}, &tokenSecret); err != nil {
return "", err
}
if tokenSecret.Data != nil {
return string(tokenSecret.Data["token"]), nil
}
return "", errors.New("token secret is empty")
}
+102 -4
View File
@@ -1,9 +1,25 @@
package k3s
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"net/http"
"path/filepath"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
corev1 "k8s.io/api/core/v1"
"github.com/rancher/k3k/pkg/controller"
)
const (
TLSDir = "/var/lib/rancher/k3s/server/tls/"
)
type BootstrapData struct {
@@ -20,20 +36,29 @@ type cert struct {
Content string
}
func GetServerBootstrap(c *Client) (*BootstrapData, error) {
type K3SConfig struct {
ClusterInit bool `json:"ClusterInit"`
}
func (c *Client) GetServerConfig() (*K3SConfig, error) {
endpoint := "/v1-k3s/config"
return do[*K3SConfig](c, endpoint, "node", http.MethodGet)
}
func (c *Client) GetServerBootstrap() (*BootstrapData, error) {
endpoint := "/v1-k3s/server-bootstrap"
bootstrap, err := do[BootstrapData](c, endpoint, "server", http.MethodGet)
bootstrap, err := do[*BootstrapData](c, endpoint, "server", http.MethodGet)
if err != nil {
return nil, err
}
// we still need to decode each certs since the bootstrap data endpoint base64 encode each cert
if err := decode(&bootstrap); err != nil {
if err := decode(bootstrap); err != nil {
return nil, fmt.Errorf("failed to decode bootstrap secret: %w", err)
}
return &bootstrap, nil
return bootstrap, nil
}
func decode(data *BootstrapData) error {
@@ -87,3 +112,76 @@ func decode(data *BootstrapData) error {
return nil
}
func ReadBootstrapFromK3sPod(ctx context.Context, restConfig *rest.Config, clusterName, clusterNamespace string) (*BootstrapData, error) {
clientset, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return nil, err
}
// using the first server in the statefulset to get the bootstrap data
serverPodName := controller.SafeConcatNameWithPrefix(clusterName, "server-0")
// skipping etcd since reading from pod only required with external datastore
bootstrapCerts := map[string]string{
"server-ca.crt": "",
"server-ca.key": "",
"client-ca.crt": "",
"client-ca.key": "",
}
for certName := range bootstrapCerts {
command := []string{"cat", filepath.Join(TLSDir, certName)}
certData, err := podExec(ctx, clientset, restConfig, clusterNamespace, serverPodName, command)
if err != nil {
return nil, err
}
bootstrapCerts[certName] = string(certData)
}
bootstrap := &BootstrapData{
ServerCA: cert{Content: bootstrapCerts["server-ca.crt"]},
ServerCAKey: cert{Content: bootstrapCerts["server-ca.key"]},
ClientCA: cert{Content: bootstrapCerts["client-ca.crt"]},
ClientCAKey: cert{Content: bootstrapCerts["client-ca.key"]},
}
return bootstrap, nil
}
func podExec(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, namespace, name string, command []string) ([]byte, error) {
req := clientset.CoreV1().RESTClient().Post().
Resource("pods").
Name(name).
Namespace(namespace).
SubResource("exec")
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
return nil, fmt.Errorf("error adding to scheme: %v", err)
}
parameterCodec := runtime.NewParameterCodec(scheme)
req.VersionedParams(&corev1.PodExecOptions{
Command: command,
Stdout: true,
}, parameterCodec)
exec, err := remotecommand.NewSPDYExecutor(config, http.MethodPost, req.URL())
if err != nil {
return nil, fmt.Errorf("error while creating Executor: %v", err)
}
var stdout bytes.Buffer
err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{
Stdout: &stdout,
})
if err != nil {
return nil, fmt.Errorf("error in Stream: %v", err)
}
return stdout.Bytes(), nil
}
+152
View File
@@ -0,0 +1,152 @@
package k3s
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_GetServerConfig(t *testing.T) {
tests := []struct {
name string
serverResponse string
clientConfig ClientConfig
expectedConfig *K3SConfig
expectedErr error
isServerRunning bool
}{
{
name: "server not ready",
isServerRunning: false,
serverResponse: "",
expectedConfig: nil,
expectedErr: ErrServerNotReady,
clientConfig: ClientConfig{
ServerIP: "127.0.0.1:33333",
},
},
{
name: "cluster init is true",
isServerRunning: true,
serverResponse: `{"ClusterInit": true}`,
expectedConfig: &K3SConfig{ClusterInit: true},
expectedErr: ErrServerNotReady,
clientConfig: ClientConfig{},
},
{
name: "cluster init is false",
isServerRunning: true,
serverResponse: `{"ClusterInit": false}`,
expectedConfig: &K3SConfig{ClusterInit: false},
expectedErr: ErrServerNotReady,
clientConfig: ClientConfig{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mux := http.NewServeMux()
mockServer := httptest.NewUnstartedServer(mux)
if tt.isServerRunning {
mockServer.StartTLS()
defer mockServer.Close()
}
mux.Handle("/v1-k3s/config", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(tt.serverResponse))
require.NoError(t, err)
}))
if tt.clientConfig.ServerIP == "" {
u, err := url.Parse(mockServer.URL)
require.NoError(t, err)
tt.clientConfig.ServerIP = u.Host
}
k3sClient := New(tt.clientConfig)
k3sConfig, err := k3sClient.GetServerConfig()
if err != nil && tt.expectedErr != nil {
assert.EqualError(t, err, tt.expectedErr.Error())
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.expectedConfig, k3sConfig)
})
}
}
func Test_GetServerBootstrap(t *testing.T) {
fakeBootstrap := &BootstrapData{
ClientCA: cert{
Content: "dGVzdA==",
},
ClientCAKey: cert{
Content: "dGVzdA==",
},
ServerCA: cert{
Content: "dGVzdA==",
},
ServerCAKey: cert{
Content: "dGVzdA==",
},
ETCDServerCA: cert{
Content: "dGVzdA==",
},
ETCDServerCAKey: cert{
Content: "dGVzdA==",
},
}
expectedBootstrap := &BootstrapData{
ClientCA: cert{
Content: "test",
},
ClientCAKey: cert{
Content: "test",
},
ServerCA: cert{
Content: "test",
},
ServerCAKey: cert{
Content: "test",
},
ETCDServerCA: cert{
Content: "test",
},
ETCDServerCAKey: cert{
Content: "test",
},
}
mux := http.NewServeMux()
mockServer := httptest.NewTLSServer(mux)
defer mockServer.Close()
serverResponse, err := json.Marshal(fakeBootstrap)
require.NoError(t, err)
mux.Handle("/v1-k3s/server-bootstrap", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write(serverResponse)
require.NoError(t, err)
}))
u, err := url.Parse(mockServer.URL)
require.NoError(t, err)
k3sClient := New(ClientConfig{ServerIP: u.Host})
bootstrap, err := k3sClient.GetServerBootstrap()
require.NoError(t, err)
assert.Equal(t, expectedBootstrap, bootstrap)
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"net/http"
)
func GetServingKubeletCrt(c *Client) (*tls.Certificate, error) {
func (c *Client) GetServingKubeletCrt() (*tls.Certificate, error) {
endpoint := "/v1-k3s/serving-kubelet.crt"
tlsCrtData, err := c.do(endpoint, "node", http.MethodGet)
+58
View File
@@ -0,0 +1,58 @@
package k3s
import (
"crypto/tls"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
fakeCertPEM = `-----BEGIN CERTIFICATE-----
MIIBgTCCASegAwIBAgIUDZWXjBG6lXVaJ7cv3NAaaCX4QxkwCgYIKoZIzj0EAwIw
FjEUMBIGA1UEAwwLazNrLWt1YmVsZXQwHhcNMjYwNjExMTEwMzQ3WhcNMzYwNjA4
MTEwMzQ3WjAWMRQwEgYDVQQDDAtrM2sta3ViZWxldDBZMBMGByqGSM49AgEGCCqG
SM49AwEHA0IABFrEAV6qpR7m8VUXL1mL9/bmuLa1QvXkiUXhWvuJ+dg7G3p1kSNC
35d4w3IAN626oyMpMD1FL9kw5U6Gd17bcoSjUzBRMB0GA1UdDgQWBBS9KJuhT7sK
3nwpq1W6KkJ34PRsrDAfBgNVHSMEGDAWgBS9KJuhT7sK3nwpq1W6KkJ34PRsrDAP
BgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIQCEg8kpyTuhsbvj9+p+
B+pbwZb+fgGO3iYuawrvVYZwHAIgIu6PYkP0ZGsUjhMkZUUNfQgZ42Lwq1CmKnqv
rWa+ZUI=
-----END CERTIFICATE-----
`
fakeKeyPEM = `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg/030b66JWbB6xK5m
1UuPpRbu2OLo7NyZQ1f/u1mxfq+hRANCAARaxAFeqqUe5vFVFy9Zi/f25ri2tUL1
5IlF4Vr7ifnYOxt6dZEjQt+XeMNyADetuqMjKTA9RS/ZMOVOhnde23KE
-----END PRIVATE KEY-----
`
)
func Test_GetServingKubeletCert(t *testing.T) {
mux := http.NewServeMux()
mockServer := httptest.NewTLSServer(mux)
defer mockServer.Close()
mux.Handle("/v1-k3s/serving-kubelet.crt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(fakeCertPEM + fakeKeyPEM))
require.NoError(t, err)
}))
u, err := url.Parse(mockServer.URL)
require.NoError(t, err)
k3sClient := New(ClientConfig{ServerIP: u.Host})
expectedCert, err := tls.X509KeyPair([]byte(fakeCertPEM), []byte(fakeKeyPEM))
require.NoError(t, err)
cert, err := k3sClient.GetServingKubeletCrt()
require.NoError(t, err)
assert.Equal(t, expectedCert, *cert)
}
+8 -2
View File
@@ -27,6 +27,12 @@ type Client struct {
var ErrServerNotReady = errors.New("server not ready")
const (
k3sNodePasswordHeader = "k3s-Node-Password"
k3sNodeIPHeader = "k3s-Node-IP"
k3sNodeNameHeader = "k3s-Node-Name"
)
func New(config ClientConfig) *Client {
httpClient := &http.Client{
Transport: http.DefaultTransport,
@@ -44,11 +50,11 @@ func New(config ClientConfig) *Client {
headers := http.Header{}
if config.Token != "" {
headers.Set("k3s-Node-Password", config.Token)
headers.Set(k3sNodePasswordHeader, config.Token)
}
if config.NodeName != "" {
headers.Set("k3s-Node-Name", config.NodeName)
headers.Set(k3sNodeNameHeader, config.NodeName)
}
var nodeIPs []string
+50
View File
@@ -0,0 +1,50 @@
package k3s
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_NewClient(t *testing.T) {
tests := []struct {
name string
clientConfig ClientConfig
expectedHeaders http.Header
}{
{
name: "empty config",
clientConfig: ClientConfig{},
expectedHeaders: nil,
},
{
name: "client config passed",
clientConfig: ClientConfig{
Token: "test_token",
ServerIP: "0.0.0.0:1234",
NodeName: "test_node",
AgentIP: "1.1.1.1",
PodIP: "2.2.2.2",
},
expectedHeaders: map[string][]string{
k3sNodePasswordHeader: {"test_token"},
k3sNodeNameHeader: {"test_node"},
k3sNodeIPHeader: {"1.1.1.1,2.2.2.2"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := New(tt.clientConfig)
assert.Len(t, client.staticHeaders, len(tt.expectedHeaders))
for expectedHeader, expectedValue := range tt.expectedHeaders {
value, ok := client.staticHeaders[http.CanonicalHeaderKey(expectedHeader)]
assert.True(t, ok, "expected header %s is not found", expectedHeader)
assert.Equal(t, expectedValue, value)
}
})
}
}