Skip to content

Commit fe8d1b6

Browse files
feat(kurl-migration): initialize kurl migration (#3207)
* detect if kurl installation is present * set correct configmap name * refactor utilities for kurl migration into separate package * address feedback * address feedback * address feedback * f * address feedback * f * improve dry-run test * fix dry-run tests * f * address feedback * run gofmt * refactor dry-run type methods * address feedback
1 parent 9e09fec commit fe8d1b6

File tree

10 files changed

+662
-39
lines changed

10 files changed

+662
-39
lines changed

cmd/installer/cli/migration.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
6+
"github.com/sirupsen/logrus"
7+
8+
"github.com/replicatedhq/embedded-cluster/pkg-new/kurl"
9+
"github.com/replicatedhq/embedded-cluster/pkg/kubeutils"
10+
)
11+
12+
// detectKurlMigration checks if this is a kURL cluster that needs migration to EC.
13+
//
14+
// Migration detection works by checking two SEPARATE clusters:
15+
// 1. kURL cluster - accessed via /etc/kubernetes/admin.conf
16+
// 2. EC cluster - accessed via EC's kubeconfig path (if it exists)
17+
//
18+
// The migration scenario is: kURL cluster exists, but EC cluster does not.
19+
//
20+
// Returns:
21+
// - (true, nil): Migration is needed (kURL cluster exists without EC cluster)
22+
// - (false, nil): Not a migration scenario, caller should continue with normal upgrade
23+
// - (false, error): Detection failed
24+
func detectKurlMigration(ctx context.Context) (bool, error) {
25+
// Check if this is a kURL cluster
26+
kurlCfg, err := kurl.GetConfig(ctx)
27+
if err != nil {
28+
return false, err
29+
}
30+
if kurlCfg == nil {
31+
return false, nil // Not kURL, continue normally
32+
}
33+
34+
logrus.Debugf("Detected kURL cluster with install directory: %s", kurlCfg.InstallDir)
35+
36+
// Check if EC is already installed (checks separate EC cluster)
37+
ecInstalled, err := isECInstalled(ctx)
38+
if err != nil {
39+
return false, err
40+
}
41+
if ecInstalled {
42+
logrus.Debugf("Embedded Cluster already installed, proceeding with normal upgrade")
43+
return false, nil // EC already installed, do normal upgrade
44+
}
45+
46+
// Migration needed - kURL cluster exists without EC cluster
47+
return true, nil
48+
}
49+
50+
// isECInstalled checks if Embedded Cluster is already installed by checking for
51+
// an EC Installation resource.
52+
func isECInstalled(ctx context.Context) (bool, error) {
53+
// Try to create a client using EC kubeconfig (separate from kURL cluster)
54+
// Using kubeutils.KubeClient() allows this to work with dryrun tests
55+
kcli, err := kubeutils.KubeClient()
56+
if err != nil {
57+
// EC kubeconfig doesn't exist or can't connect - not installed
58+
return false, nil
59+
}
60+
61+
// Check if Installation CRD exists by trying to get the latest installation
62+
// This leverages the existing kubeutils.GetLatestInstallation function
63+
// which returns ErrNoInstallations{} if no installations exist
64+
_, err = kubeutils.GetLatestInstallation(ctx, kcli)
65+
if err != nil {
66+
// If the error is ErrNoInstallations, EC is not installed (normal case)
67+
if _, ok := err.(kubeutils.ErrNoInstallations); ok {
68+
return false, nil
69+
}
70+
return false, err
71+
}
72+
73+
// Installation exists, EC is installed
74+
return true, nil
75+
}

cmd/installer/cli/upgrade.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/replicatedhq/embedded-cluster/pkg-new/replicatedapi"
2020
"github.com/replicatedhq/embedded-cluster/pkg-new/validation"
2121
"github.com/replicatedhq/embedded-cluster/pkg/airgap"
22+
"github.com/replicatedhq/embedded-cluster/pkg/dryrun"
2223
"github.com/replicatedhq/embedded-cluster/pkg/helpers"
2324
"github.com/replicatedhq/embedded-cluster/pkg/kubeutils"
2425
"github.com/replicatedhq/embedded-cluster/pkg/metrics"
@@ -87,14 +88,35 @@ func UpgradeCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command {
8788
return fmt.Errorf(`invalid --target (must be: "linux")`)
8889
}
8990

90-
if os.Getuid() != 0 {
91+
// Skip root check if dryrun mode is enabled
92+
if !dryrun.Enabled() && os.Getuid() != 0 {
9193
return fmt.Errorf("upgrade command must be run as root")
9294
}
9395

9496
// set the umask to 022 so that we can create files/directories with 755 permissions
9597
// this does not return an error - it returns the previous umask
9698
_ = syscall.Umask(0o022)
9799

100+
// Check if this is a kURL cluster that needs migration to Embedded Cluster.
101+
migrationNeeded, err := detectKurlMigration(ctx)
102+
if err != nil {
103+
return fmt.Errorf("failed to detect migration scenario: %w", err)
104+
}
105+
106+
if migrationNeeded {
107+
logrus.Info("Preparing to upgrade to Embedded Cluster...")
108+
logrus.Info("")
109+
logrus.Info("This upgrade will be available in a future release.")
110+
logrus.Info("")
111+
// TBD: In a future story, this will:
112+
// 1. Export the kURL password hash for authentication compatibility
113+
// 2. Generate TLS certificates for the migration API
114+
// 3. Start the Admin Console API in migration mode
115+
// 4. Display the manager URL for the user to complete the upgrade via UI
116+
// 5. Block until the user completes the upgrade or interrupts (Ctrl+C)
117+
return nil
118+
}
119+
98120
// Set up environment variables from existing runtime config
99121
existingRC, err := rcutil.GetRuntimeConfigFromCluster(ctx)
100122
if err != nil {

pkg-new/kurl/kurl.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package kurl
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
corev1 "k8s.io/api/core/v1"
9+
apierrors "k8s.io/apimachinery/pkg/api/errors"
10+
"sigs.k8s.io/controller-runtime/pkg/client"
11+
12+
"github.com/replicatedhq/embedded-cluster/pkg/kubeutils"
13+
)
14+
15+
const (
16+
ConfigMapName = "kurl-config"
17+
ConfigMapKey = "kurl_install_directory"
18+
DefaultInstallDir = "/var/lib/kurl"
19+
KotsadmNamespace = "kotsadm"
20+
KubeSystemNamespace = "kube-system"
21+
KotsadmPasswordSecret = "kotsadm-password"
22+
KotsadmPasswordSecretKey = "passwordBcrypt"
23+
)
24+
25+
// Config holds configuration information about a kURL cluster.
26+
type Config struct {
27+
// Client is a kubernetes client authenticated to the kURL cluster.
28+
Client client.Client
29+
// InstallDir is the directory where kURL installed its assets.
30+
InstallDir string
31+
}
32+
33+
// GetConfig attempts to detect and return configuration for a kURL cluster.
34+
// Returns nil if no kURL cluster is detected, or an error if detection fails.
35+
func GetConfig(ctx context.Context) (*Config, error) {
36+
// Check if kURL's kubeconfig file exists
37+
if _, err := os.Stat(kubeutils.KURLKubeconfigPath); err != nil {
38+
if os.IsNotExist(err) {
39+
// File doesn't exist - not a kURL cluster
40+
return nil, nil
41+
}
42+
// File exists but can't stat it - that's an error
43+
return nil, fmt.Errorf("failed to check kurl kubeconfig at %s: %w", kubeutils.KURLKubeconfigPath, err)
44+
}
45+
46+
// Create kURL client (will use the same path logic)
47+
kcli, err := kubeutils.KURLKubeClient()
48+
if err != nil {
49+
return nil, fmt.Errorf("failed to create kurl client: %w", err)
50+
}
51+
52+
// Check for kURL ConfigMap and get install directory
53+
installDir, err := getInstallDirectory(ctx, kcli)
54+
if err != nil {
55+
if apierrors.IsNotFound(err) {
56+
return nil, nil
57+
}
58+
return nil, fmt.Errorf("failed to check for kurl configmap: %w", err)
59+
}
60+
61+
return &Config{
62+
Client: kcli,
63+
InstallDir: installDir,
64+
}, nil
65+
}
66+
67+
// getInstallDirectory reads the kURL ConfigMap in kube-system namespace
68+
// to determine the actual kURL installer directory (which may be customized).
69+
// Returns the configured directory or the default if not specified.
70+
func getInstallDirectory(ctx context.Context, kcli client.Client) (string, error) {
71+
cm := &corev1.ConfigMap{}
72+
err := kcli.Get(ctx, client.ObjectKey{
73+
Namespace: KubeSystemNamespace,
74+
Name: ConfigMapName,
75+
}, cm)
76+
if err != nil {
77+
// Return error directly without wrapping so apierrors.IsNotFound() works
78+
return "", err
79+
}
80+
81+
installDir, exists := cm.Data[ConfigMapKey]
82+
if !exists || installDir == "" {
83+
// Return default if not customized
84+
return DefaultInstallDir, nil
85+
}
86+
87+
return installDir, nil
88+
}
89+
90+
// GetPasswordHash reads the kotsadm-password secret from the kotsadm namespace
91+
// and returns the bcrypt password hash. This is used during migration to preserve the
92+
// existing admin console password.
93+
func GetPasswordHash(ctx context.Context, cfg *Config, namespace string) (string, error) {
94+
if namespace == "" {
95+
namespace = KotsadmNamespace
96+
}
97+
98+
secret := &corev1.Secret{}
99+
err := cfg.Client.Get(ctx, client.ObjectKey{
100+
Namespace: namespace,
101+
Name: KotsadmPasswordSecret,
102+
}, secret)
103+
if err != nil {
104+
return "", fmt.Errorf("read kotsadm-password secret from cluster: %w", err)
105+
}
106+
107+
passwordHash, exists := secret.Data[KotsadmPasswordSecretKey]
108+
if !exists || len(passwordHash) == 0 {
109+
return "", fmt.Errorf("kotsadm-password secret is missing required passwordBcrypt data")
110+
}
111+
112+
return string(passwordHash), nil
113+
}

0 commit comments

Comments
 (0)