Skip to content

Commit 673564d

Browse files
authored
feat: auto-migrate k3s to k8s (#2654)
1 parent 1458094 commit 673564d

File tree

7 files changed

+286
-11
lines changed

7 files changed

+286
-11
lines changed

config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ func ValidateChanges(oldCfg, newCfg *Config) error {
325325

326326
// ValidateStoreAndDistroChanges checks whether migrating from one store to the other is allowed.
327327
func ValidateStoreAndDistroChanges(currentStoreType, previousStoreType StoreType, currentDistro, previousDistro string) error {
328-
if currentDistro != previousDistro && !(previousDistro == "eks" && currentDistro == K8SDistro) {
328+
if currentDistro != previousDistro && !(previousDistro == "eks" && currentDistro == K8SDistro) && !(previousDistro == K3SDistro && currentDistro == K8SDistro) {
329329
return fmt.Errorf("seems like you were using %s as a distro before and now have switched to %s, please make sure to not switch between vCluster distros", previousDistro, currentDistro)
330330
}
331331

pkg/certs/constants.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ const (
2929
// CAKeyName defines certificate name
3030
CAKeyName = "ca.key"
3131

32+
// ServerCAKeyName defines server ca key name
33+
ServerCAKeyName = "server-ca.key"
34+
// ServerCACertName defines server ca cert name
35+
ServerCACertName = "server-ca.crt"
36+
37+
// ClientCACertName defines client ca cert name
38+
ClientCACertName = "client-ca.crt"
39+
// ClientCAKeyName defines client ca key name
40+
ClientCAKeyName = "client-ca.key"
41+
3242
// APIServerCertAndKeyBaseName defines API's server certificate and key base name
3343
APIServerCertAndKeyBaseName = "apiserver"
3444
// APIServerCertName defines API's server certificate name
@@ -148,6 +158,12 @@ var certMap = map[string]string{
148158
CACertName: CACertName,
149159
CAKeyName: CAKeyName,
150160

161+
ServerCACertName: ServerCACertName,
162+
ServerCAKeyName: ServerCAKeyName,
163+
164+
ClientCACertName: ClientCACertName,
165+
ClientCAKeyName: ClientCAKeyName,
166+
151167
FrontProxyCACertName: FrontProxyCACertName,
152168
FrontProxyCAKeyName: FrontProxyCAKeyName,
153169

pkg/certs/ensure.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ func EnsureCerts(
4242
return errors.New("nil currentNamespaceClient")
4343
}
4444

45-
// we create a certificate for up to 20 etcd replicas, this should be sufficient for most use cases. Eventually we probably
45+
// we create a certificate for up to 5 etcd replicas, this should be sufficient for most use cases. Eventually we probably
4646
// want to update this to the actual etcd number, but for now this is the easiest way to allow up and downscaling without
4747
// regenerating certificates.
48-
secretName := vClusterName + "-certs"
48+
secretName := CertSecretName(vClusterName)
4949
secret, err := currentNamespaceClient.CoreV1().Secrets(currentNamespace).Get(ctx, secretName, metav1.GetOptions{})
5050
if err == nil {
5151
// download certs from secret
@@ -181,6 +181,10 @@ func EnsureCerts(
181181
return downloadCertsFromSecret(secret, certificateDir)
182182
}
183183

184+
func CertSecretName(vClusterName string) string {
185+
return vClusterName + "-certs"
186+
}
187+
184188
func createConfig(serviceCIDR, vClusterName, certificateDir, clusterDomain string, etcdSans []string) (*InitConfiguration, error) {
185189
// init config
186190
cfg, err := SetInitDynamicDefaults()
@@ -228,6 +232,11 @@ func generateCertificates(
228232
return fmt.Errorf("create kube configs: %w", err)
229233
}
230234

235+
err = splitCACert(certificateDir)
236+
if err != nil {
237+
return fmt.Errorf("split ca cert: %w", err)
238+
}
239+
231240
return nil
232241
}
233242

@@ -267,6 +276,46 @@ func downloadCertsFromSecret(
267276
}
268277
}
269278

279+
err := splitCACert(certificateDir)
280+
if err != nil {
281+
return fmt.Errorf("split ca cert: %w", err)
282+
}
283+
284+
return nil
285+
}
286+
287+
func splitCACert(certificateDir string) error {
288+
// make sure to write server-ca and client-ca to file system
289+
err := copyFileIfNotExists(filepath.Join(certificateDir, CACertName), filepath.Join(certificateDir, ServerCACertName))
290+
if err != nil {
291+
return fmt.Errorf("copy %s: %w", ServerCACertName, err)
292+
}
293+
err = copyFileIfNotExists(filepath.Join(certificateDir, CAKeyName), filepath.Join(certificateDir, ServerCAKeyName))
294+
if err != nil {
295+
return fmt.Errorf("copy %s: %w", ServerCAKeyName, err)
296+
}
297+
err = copyFileIfNotExists(filepath.Join(certificateDir, CACertName), filepath.Join(certificateDir, ClientCACertName))
298+
if err != nil {
299+
return fmt.Errorf("copy %s: %w", ClientCACertName, err)
300+
}
301+
err = copyFileIfNotExists(filepath.Join(certificateDir, CAKeyName), filepath.Join(certificateDir, ClientCAKeyName))
302+
if err != nil {
303+
return fmt.Errorf("copy %s: %w", ClientCAKeyName, err)
304+
}
305+
306+
return nil
307+
}
308+
309+
func copyFileIfNotExists(src, dst string) error {
310+
_, err := os.Stat(dst)
311+
if os.IsNotExist(err) {
312+
srcBytes, err := os.ReadFile(src)
313+
if err != nil {
314+
return fmt.Errorf("read %s: %w", src, err)
315+
}
316+
317+
return os.WriteFile(dst, srcBytes, 0666)
318+
}
270319
return nil
271320
}
272321

pkg/config/config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ func (v VirtualClusterConfig) VirtualClusterKubeConfig() config.VirtualClusterKu
7575
case config.K8SDistro:
7676
distroConfig = config.VirtualClusterKubeConfig{
7777
KubeConfig: "/data/pki/admin.conf",
78-
ServerCAKey: "/data/pki/ca.key",
79-
ServerCACert: "/data/pki/ca.crt",
80-
ClientCACert: "/data/pki/ca.crt",
78+
ServerCAKey: "/data/pki/server-ca.key",
79+
ServerCACert: "/data/pki/server-ca.crt",
80+
ClientCACert: "/data/pki/client-ca.crt",
8181
RequestHeaderCACert: "/data/pki/front-proxy-ca.crt",
8282
}
8383
}

pkg/k8s/migrate.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package k8s
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
vclusterconfig "github.com/loft-sh/vcluster/config"
9+
"github.com/loft-sh/vcluster/pkg/certs"
10+
"github.com/loft-sh/vcluster/pkg/config"
11+
"github.com/loft-sh/vcluster/pkg/constants"
12+
kerrors "k8s.io/apimachinery/pkg/api/errors"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/client-go/kubernetes"
15+
"k8s.io/client-go/tools/clientcmd"
16+
"k8s.io/klog/v2"
17+
)
18+
19+
var (
20+
migratedFromK3sAnnotation = "vcluster.loft.sh/migrated-from-k3s"
21+
22+
k3sKubeConfig = map[string]string{
23+
certs.AdminKubeConfigFileName: "/data/server/cred/admin.kubeconfig",
24+
certs.ControllerManagerKubeConfigFileName: "/data/server/cred/controller.kubeconfig",
25+
certs.SchedulerKubeConfigFileName: "/data/server/cred/scheduler.kubeconfig",
26+
}
27+
28+
k3sTLS = map[string]string{
29+
certs.APIServerCertName: "/data/server/tls/serving-kube-apiserver.crt",
30+
certs.APIServerKeyName: "/data/server/tls/serving-kube-apiserver.key",
31+
32+
certs.ServerCACertName: "/data/server/tls/server-ca.crt",
33+
certs.ServerCAKeyName: "/data/server/tls/server-ca.key",
34+
35+
certs.ClientCACertName: "/data/server/tls/client-ca.crt",
36+
certs.ClientCAKeyName: "/data/server/tls/client-ca.key",
37+
38+
certs.FrontProxyCACertName: "/data/server/tls/request-header-ca.crt",
39+
certs.FrontProxyCAKeyName: "/data/server/tls/request-header-ca.key",
40+
41+
certs.FrontProxyClientCertName: "/data/server/tls/client-auth-proxy.crt",
42+
certs.FrontProxyClientKeyName: "/data/server/tls/client-auth-proxy.key",
43+
44+
certs.ServiceAccountPrivateKeyName: "/data/server/tls/service.current.key",
45+
certs.ServiceAccountPublicKeyName: "/data/server/tls/service.key",
46+
}
47+
)
48+
49+
func MigrateK3sToK8s(ctx context.Context, currentNamespaceClient kubernetes.Interface, currentNamespace string, options *config.VirtualClusterConfig) error {
50+
// only migrate if we are migrating from k3s to k8s
51+
if options.Distro() != vclusterconfig.K8SDistro {
52+
return nil
53+
} else if _, err := os.Stat("/data/server/tls"); err != nil { // fast path
54+
return nil
55+
}
56+
57+
// migrate data first
58+
if options.BackingStoreType() == vclusterconfig.StoreTypeEmbeddedDatabase || options.BackingStoreType() == vclusterconfig.StoreTypeEmbeddedEtcd {
59+
// copy over the data
60+
err := renameIfExists(constants.K3sSqliteDatabase, constants.K8sSqliteDatabase)
61+
if err != nil {
62+
return fmt.Errorf("failed to rename sqlite database: %w", err)
63+
}
64+
err = renameIfExists(constants.K3sSqliteDatabase+"-wal", constants.K8sSqliteDatabase+"-wal")
65+
if err != nil {
66+
return fmt.Errorf("failed to rename sqlite database: %w", err)
67+
}
68+
err = renameIfExists(constants.K3sSqliteDatabase+"-shm", constants.K8sSqliteDatabase+"-shm")
69+
if err != nil {
70+
return fmt.Errorf("failed to rename sqlite database: %w", err)
71+
}
72+
}
73+
74+
// get the secret
75+
secret, err := currentNamespaceClient.CoreV1().Secrets(currentNamespace).Get(ctx, certs.CertSecretName(options.Name), metav1.GetOptions{})
76+
if err != nil {
77+
if kerrors.IsNotFound(err) {
78+
// this is fine, we can just skip this
79+
return nil
80+
}
81+
82+
return fmt.Errorf("failed to get certificate secret %s: %w", certs.CertSecretName(options.Name), err)
83+
} else if secret.Annotations[migratedFromK3sAnnotation] == "true" { // already migrated
84+
return nil
85+
}
86+
87+
// convert tls secrets
88+
for inSecretName, fileName := range k3sKubeConfig {
89+
if _, err := os.Stat(fileName); os.IsNotExist(err) {
90+
return nil
91+
}
92+
93+
secret.Data[inSecretName], err = fillKubeConfig(fileName)
94+
if err != nil {
95+
klog.Errorf("failed to fill k3s kube config %s: %s", fileName, err)
96+
return err
97+
}
98+
}
99+
100+
// convert kube configs
101+
for inSecretName, fileName := range k3sTLS {
102+
if _, err := os.Stat(fileName); os.IsNotExist(err) {
103+
return nil
104+
}
105+
106+
secret.Data[inSecretName], err = os.ReadFile(fileName)
107+
if err != nil {
108+
klog.Errorf("failed to read k3s tls secret %s: %s", fileName, err)
109+
return err
110+
}
111+
}
112+
113+
// update secret
114+
klog.Infof("Migrating k3s distro certificates to k8s...")
115+
if secret.Annotations == nil {
116+
secret.Annotations = make(map[string]string)
117+
}
118+
secret.Annotations[migratedFromK3sAnnotation] = "true"
119+
_, err = currentNamespaceClient.CoreV1().Secrets(secret.Namespace).Update(ctx, secret, metav1.UpdateOptions{})
120+
if err != nil {
121+
if kerrors.IsConflict(err) {
122+
klog.Infof("failed to migrate k3s tls secret %s: %s, retrying", secret.Name, err)
123+
124+
// get the secret again
125+
secret, err = currentNamespaceClient.CoreV1().Secrets(secret.Namespace).Get(ctx, secret.Name, metav1.GetOptions{})
126+
if err != nil {
127+
klog.Errorf("failed to get k3s tls secret %s: %s", secret.Name, err)
128+
return err
129+
}
130+
131+
return MigrateK3sToK8s(ctx, currentNamespaceClient, currentNamespace, options)
132+
}
133+
134+
klog.Errorf("failed to migrate k3s tls secret %s: %s", secret.Name, err)
135+
return err
136+
}
137+
138+
// remove old data
139+
_ = os.RemoveAll("/data/server")
140+
return nil
141+
}
142+
143+
func renameIfExists(old, new string) error {
144+
if _, err := os.Stat(old); os.IsNotExist(err) {
145+
return nil
146+
}
147+
148+
return os.Rename(old, new)
149+
}
150+
151+
func fillKubeConfig(kubeConfigPath string) ([]byte, error) {
152+
config, err := clientcmd.LoadFromFile(kubeConfigPath)
153+
if err != nil {
154+
return nil, err
155+
}
156+
157+
// exchange kube config server & resolve certificate
158+
for _, cluster := range config.Clusters {
159+
if cluster == nil {
160+
continue
161+
}
162+
163+
// fill in data
164+
if cluster.CertificateAuthorityData == nil && cluster.CertificateAuthority != "" {
165+
o, err := os.ReadFile(cluster.CertificateAuthority)
166+
if err != nil {
167+
return nil, err
168+
}
169+
170+
cluster.CertificateAuthority = ""
171+
cluster.CertificateAuthorityData = o
172+
}
173+
174+
cluster.Server = "https://127.0.0.1:6443"
175+
}
176+
177+
// resolve auth info cert & key
178+
for _, authInfo := range config.AuthInfos {
179+
if authInfo == nil {
180+
continue
181+
}
182+
183+
// fill in data
184+
if authInfo.ClientCertificateData == nil && authInfo.ClientCertificate != "" {
185+
o, err := os.ReadFile(authInfo.ClientCertificate)
186+
if err != nil {
187+
return nil, err
188+
}
189+
190+
authInfo.ClientCertificate = ""
191+
authInfo.ClientCertificateData = o
192+
}
193+
if authInfo.ClientKeyData == nil && authInfo.ClientKey != "" {
194+
o, err := os.ReadFile(authInfo.ClientKey)
195+
if err != nil {
196+
return nil, err
197+
}
198+
199+
authInfo.ClientKey = ""
200+
authInfo.ClientKeyData = o
201+
}
202+
}
203+
204+
return clientcmd.Write(*config)
205+
}

pkg/setup/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func EnsureBackingStoreChanges(ctx context.Context, client kubernetes.Interface,
122122
// It checks for the existence of the default K3s token path or the K0s data directory.
123123
func CheckUsingHeuristic(distro string) (bool, error) {
124124
// check if previously we were using k3s as a default and now have switched to a different distro
125-
if distro != vclusterconfig.K3SDistro {
125+
if distro != vclusterconfig.K3SDistro && distro != vclusterconfig.K8SDistro {
126126
_, err := os.Stat(k3s.TokenPath)
127127
if err == nil {
128128
return false, fmt.Errorf("seems like you were using k3s as a distro before and now have switched to %s, please make sure to not switch between vCluster distros", distro)

pkg/setup/initialize.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,14 @@ import (
3131

3232
// Initialize creates the required secrets and configmaps for the control plane to start
3333
func Initialize(ctx context.Context, options *config.VirtualClusterConfig) error {
34+
// migrate k3s to k8s if needed
35+
err := k8s.MigrateK3sToK8s(ctx, options.ControlPlaneClient, options.ControlPlaneNamespace, options)
36+
if err != nil {
37+
return fmt.Errorf("migrate k3s to k8s: %w", err)
38+
}
39+
3440
// Ensure that service CIDR range is written into the expected location
35-
err := wait.PollUntilContextTimeout(ctx, 5*time.Second, 2*time.Minute, true, func(waitCtx context.Context) (bool, error) {
41+
err = wait.PollUntilContextTimeout(ctx, 5*time.Second, 2*time.Minute, true, func(waitCtx context.Context) (bool, error) {
3642
err := initialize(waitCtx, options)
3743
if err != nil {
3844
klog.Errorf("error initializing service cidr, certs and token: %v", err)
@@ -253,9 +259,8 @@ func GenerateCerts(ctx context.Context, currentNamespaceClient kubernetes.Interf
253259
)
254260
}
255261

256-
// expect up to 20 etcd members, number could be lower since more
257-
// than 5 is generally a bad idea
258-
for i := range 20 {
262+
// expect up to 5 etcd members
263+
for i := range 5 {
259264
// this is for embedded etcd
260265
hostname := vClusterName + "-" + strconv.Itoa(i)
261266
etcdSans = append(etcdSans, hostname, hostname+"."+vClusterName+"-headless", hostname+"."+vClusterName+"-headless"+"."+currentNamespace)

0 commit comments

Comments
 (0)