From 8a8df4f00e0ca258d57803f34ddc62aed7af8c48 Mon Sep 17 00:00:00 2001 From: Pablo Acevedo Montserrat Date: Thu, 30 Oct 2025 14:21:44 +0100 Subject: [PATCH 1/8] OCPEDGE-2038: Rename several functions for add-node --- pkg/cmd/addnode.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/addnode.go b/pkg/cmd/addnode.go index 864876e892..fddecefaaf 100644 --- a/pkg/cmd/addnode.go +++ b/pkg/cmd/addnode.go @@ -99,7 +99,7 @@ func runAddNode(ctx context.Context, opts *AddNodeOptions) error { } nodeName := cfg.CanonicalNodeName() - if isNodeAlreadyInCluster(ctx, client, nodeName) { + if isNodeInKubernetesCluster(ctx, client, nodeName) { klog.Infof("Node %s is already part of the cluster. Skipping join process.", nodeName) return nil } @@ -126,12 +126,12 @@ func runAddNode(ctx context.Context, opts *AddNodeOptions) error { } klog.Info("Etcd certificates generated successfully") - clusterMembers, err := getClusterNodes(ctx, client) + etcdMembers, err := getEtcdClusterNodes(ctx, client) if err != nil { return fmt.Errorf("failed to get cluster information: %w", err) } - if err := configureEtcdForCluster(ctx, cfg, clusterMembers, opts.Learner); err != nil { + if err := configureEtcdForCluster(ctx, cfg, etcdMembers, opts.Learner); err != nil { return fmt.Errorf("failed to configure etcd for cluster: %w", err) } @@ -374,7 +374,7 @@ func generateEtcdCertificates(cfg *config.Config) error { return nil } -func getClusterNodes(ctx context.Context, client kubernetes.Interface) ([]string, error) { +func getEtcdClusterNodes(ctx context.Context, client kubernetes.Interface) ([]string, error) { nodes, err := client.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) if err != nil { return nil, fmt.Errorf("failed to list nodes: %w", err) @@ -382,9 +382,6 @@ func getClusterNodes(ctx context.Context, client kubernetes.Interface) ([]string var members []string for _, node := range nodes.Items { - if !isNodeReady(&node) { - continue - } nodeIP := "" for _, addr := range node.Status.Addresses { if addr.Type == corev1.NodeInternalIP { @@ -393,6 +390,7 @@ func getClusterNodes(ctx context.Context, client kubernetes.Interface) ([]string } } if nodeIP != "" { + //TODO net.JoinHostPort members = append(members, fmt.Sprintf("%s=https://%s:2380", node.Name, nodeIP)) } } @@ -532,7 +530,7 @@ func restartMicroShift() error { return nil } -func isNodeAlreadyInCluster(ctx context.Context, client kubernetes.Interface, nodeName string) bool { +func isNodeInKubernetesCluster(ctx context.Context, client kubernetes.Interface, nodeName string) bool { _, err := client.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) if err != nil { return false From b6fcb759e19af6254da5ac61eb906f1d082a65e5 Mon Sep 17 00:00:00 2001 From: Pablo Acevedo Montserrat Date: Thu, 30 Oct 2025 15:16:08 +0100 Subject: [PATCH 2/8] OCPEDGE-2038: Add promote-learner command --- cmd/microshift/main.go | 1 + pkg/cmd/promotelearner.go | 134 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 pkg/cmd/promotelearner.go diff --git a/cmd/microshift/main.go b/cmd/microshift/main.go index bf275fb316..a97eedd04a 100644 --- a/cmd/microshift/main.go +++ b/cmd/microshift/main.go @@ -42,5 +42,6 @@ func newCommand() *cobra.Command { cmd.AddCommand(cmds.NewRestoreCommand()) cmd.AddCommand(cmds.NewHealthcheckCommand()) cmd.AddCommand(cmds.NewAddNodeCommand()) + cmd.AddCommand(cmds.NewPromoteLearnerCommand()) return cmd } diff --git a/pkg/cmd/promotelearner.go b/pkg/cmd/promotelearner.go new file mode 100644 index 0000000000..643686746d --- /dev/null +++ b/pkg/cmd/promotelearner.go @@ -0,0 +1,134 @@ +package cmd + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/openshift/microshift/pkg/config" + "github.com/openshift/microshift/pkg/util/cryptomaterial" + "github.com/spf13/cobra" + "go.etcd.io/etcd/client/pkg/v3/transport" + clientv3 "go.etcd.io/etcd/client/v3" + + "k8s.io/klog/v2" +) + +func NewPromoteLearnerCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "promote-learner", + Short: "Promote learner node to member in the etcd cluster", + Long: `This command promotes the local etcd instance from a learner node to a full voting member within an existing MicroShift etcd cluster. +It: + - Connects to the etcd cluster using the current node's configuration. + - Verifies that the local etcd instance is currently a learner. + - Issues a promote request to the cluster. + - Restarts the MicroShift service to make the membership change effective to apiserver. +Use this command only after the learner node has fully caught up with the cluster and you are ready for it to become a voting member. + `, + RunE: func(cmd *cobra.Command, args []string) error { + return runPromoteLearner(cmd.Context()) + }, + } + return cmd +} + +func runPromoteLearner(ctx context.Context) error { + klog.Info("Starting learner promotion process") + + cfg, err := config.ActiveConfig() + if err != nil { + return fmt.Errorf("failed to load MicroShift configuration: %w", err) + } + + klog.Infof("Creating Kubernetes client from %s", cfg.KubeConfigPath(config.KubeAdmin)) + client, err := createKubernetesClient(cfg.KubeConfigPath(config.KubeAdmin)) + if err != nil { + return fmt.Errorf("failed to create Kubernetes client: %w", err) + } + + etcdMembers, err := getEtcdClusterNodes(ctx, client) + if err != nil { + return fmt.Errorf("failed to get cluster information: %w", err) + } + + klog.Info("Promoting etcd learner to member") + if err := promoteEtcdLearner(etcdMembers, cfg); err != nil { + return fmt.Errorf("failed to promote etcd learner: %w", err) + } + + klog.Info("Restarting MicroShift service") + if err := restartMicroShift(); err != nil { + return fmt.Errorf("failed to restart MicroShift service: %w", err) + } + klog.Info("Learner promotion process completed successfully") + return nil +} + +func promoteEtcdLearner(clusterMembers []string, cfg *config.Config) error { + certsDir := cryptomaterial.CertsDirectory(config.DataDir) + etcdPeerClientCertDir := cryptomaterial.EtcdPeerCertDir(certsDir) + + tlsInfo := transport.TLSInfo{ + CertFile: cryptomaterial.PeerCertPath(etcdPeerClientCertDir), + KeyFile: cryptomaterial.PeerKeyPath(etcdPeerClientCertDir), + TrustedCAFile: cryptomaterial.CACertPath(cryptomaterial.EtcdSignerDir(certsDir)), + } + tlsConfig, err := tlsInfo.ClientConfig() + if err != nil { + return fmt.Errorf("failed to create etcd client TLS config: %v", err) + } + var endpoints []string + for _, member := range clusterMembers { + parts := strings.SplitN(member, "=", 2) + if len(parts) == 2 { + endpoint := strings.Replace(parts[1], ":2380", ":2379", 1) + endpoints = append(endpoints, endpoint) + } + } + + client, err := clientv3.New(clientv3.Config{ + Endpoints: endpoints, + DialTimeout: 5 * time.Second, + TLS: tlsConfig, + Context: context.Background(), + }) + if err != nil { + return fmt.Errorf("failed to create etcd client: %v", err) + } + defer client.Close() + + memberResponse, err := client.MemberList(context.Background()) + if err != nil { + return fmt.Errorf("failed to list etcd members: %v", err) + } + + found, learner := false, false + var id uint64 = 0 + + for _, member := range memberResponse.Members { + if member.Name == cfg.CanonicalNodeName() { + found = true + if member.IsLearner { + learner = true + id = member.ID + } + } + } + + if !found { + return fmt.Errorf("node %s is not in the etcd cluster", cfg.CanonicalNodeName()) + } + if !learner { + return fmt.Errorf("node %s is not a learner", cfg.CanonicalNodeName()) + } + + response, err := client.MemberPromote(context.Background(), id) + if err != nil { + return fmt.Errorf("failed to promote etcd learner: %v", err) + } + klog.Infof("Successfully promoted etcd learner %s with response: %v", cfg.CanonicalNodeName(), response) + + return nil +} From 68339f8222b22b9ae2b5cc093c31075f80739f77 Mon Sep 17 00:00:00 2001 From: Pablo Acevedo Montserrat Date: Thu, 30 Oct 2025 23:22:46 +0100 Subject: [PATCH 3/8] OCPEDGE-2038: Rework kubelet controller functions --- pkg/node/kubelet.go | 143 ++++++++++++++++++++------------------- pkg/node/kubelet_test.go | 3 +- 2 files changed, 74 insertions(+), 72 deletions(-) diff --git a/pkg/node/kubelet.go b/pkg/node/kubelet.go index 6a0c8ce6d8..2d5a033863 100644 --- a/pkg/node/kubelet.go +++ b/pkg/node/kubelet.go @@ -49,26 +49,82 @@ const ( ) type KubeletServer struct { - kubeletflags *kubeletoptions.KubeletFlags - kubeconfig *kubeletconfig.KubeletConfiguration + cfg *config.Config } func NewKubeletServer(cfg *config.Config) *KubeletServer { - s := &KubeletServer{} - s.configure(cfg) - return s + return &KubeletServer{ + cfg: cfg, + } } func (s *KubeletServer) Name() string { return componentKubelet } func (s *KubeletServer) Dependencies() []string { return []string{"kube-apiserver"} } -func (s *KubeletServer) configure(cfg *config.Config) { - if err := s.writeConfig(cfg); err != nil { - klog.Fatalf("Failed to write kubelet config %v", err) +func (s *KubeletServer) Run(ctx context.Context, ready chan<- struct{}, stopped chan<- struct{}) error { + defer close(stopped) + + kubeletFlags, kubeletConfiguration, err := configure(ctx, s.cfg) + if err != nil { + return fmt.Errorf("failed to configure kubelet: %w", err) + } + + // construct a KubeletServer from kubeletFlags and kubeletConfig + kubeletServer := &kubeletoptions.KubeletServer{ + KubeletFlags: *kubeletFlags, + KubeletConfiguration: *kubeletConfiguration, + } + + kubeletDeps, err := kubelet.UnsecuredDependencies(ctx, kubeletServer, utilfeature.DefaultFeatureGate) + if err != nil { + return fmt.Errorf("error fetching dependencies: %w", err) + } + + errc := make(chan error) + + // Run healthcheck probe and kubelet in parallel. + // No matter which ends first - if it ends with an error, + // it'll cause ServiceManager to trigger graceful shutdown. + + // run readiness check + go func() { + // This endpoint does not use TLS, but reusing the same function without verification. + healthcheckStatus := util.RetryInsecureGet(ctx, "http://localhost:10248/healthz") + if healthcheckStatus != 200 { + e := fmt.Errorf("%s failed to start", s.Name()) + klog.Error(e) + errc <- e + return + } + klog.Infof("%s is ready", s.Name()) + close(ready) + }() + + panicChannel := make(chan any, 1) + go func() { + defer func() { + if r := recover(); r != nil { + panicChannel <- r + } + }() + errc <- kubelet.Run(ctx, kubeletServer, kubeletDeps, utilfeature.DefaultFeatureGate) + }() + + select { + case err := <-errc: + return err + case perr := <-panicChannel: + panic(perr) + } +} + +func configure(ctx context.Context, cfg *config.Config) (*kubeletoptions.KubeletFlags, *kubeletconfig.KubeletConfiguration, error) { + if err := writeConfig(cfg); err != nil { + return nil, nil, fmt.Errorf("failed to write kubelet config %v", err) } osID, err := loadOSID() if err != nil { - klog.Fatalf("Failed to read OS ID %v", err) + return nil, nil, fmt.Errorf("failed to read OS ID %v", err) } nodeIP := cfg.Node.NodeIP @@ -90,18 +146,17 @@ func (s *KubeletServer) configure(cfg *config.Config) { kubeletFlags.NodeLabels["node.openshift.io/os_id"] = osID kubeletFlags.NodeLabels["node.kubernetes.io/instance-type"] = "rhde" - kubeletConfig, err := loadConfigFile(filepath.Join(config.DataDir, "/resources/kubelet/config/config.yaml")) + kubeletConfig, err := loadConfigFile(ctx, filepath.Join(config.DataDir, "/resources/kubelet/config/config.yaml")) if err != nil { - klog.Fatalf("Failed to load Kubelet Configuration %v", err) + return nil, nil, fmt.Errorf("failed to load Kubelet configuration %v", err) } - s.kubeconfig = kubeletConfig - s.kubeletflags = kubeletFlags + return kubeletFlags, kubeletConfig, nil } -func (s *KubeletServer) writeConfig(cfg *config.Config) error { - data, err := s.generateConfig(cfg) +func writeConfig(cfg *config.Config) error { + data, err := generateConfig(cfg) if err != nil { return err } @@ -114,7 +169,7 @@ func (s *KubeletServer) writeConfig(cfg *config.Config) error { return os.WriteFile(path, data, 0400) } -func (s *KubeletServer) generateConfig(cfg *config.Config) ([]byte, error) { +func generateConfig(cfg *config.Config) ([]byte, error) { certsDir := cryptomaterial.CertsDirectory(config.DataDir) servingCertDir := cryptomaterial.KubeletServingCertDir(certsDir) @@ -164,59 +219,7 @@ func (s *KubeletServer) generateConfig(cfg *config.Config) ([]byte, error) { return data.Bytes(), nil } -func (s *KubeletServer) Run(ctx context.Context, ready chan<- struct{}, stopped chan<- struct{}) error { - defer close(stopped) - - // construct a KubeletServer from kubeletFlags and kubeletConfig - kubeletServer := &kubeletoptions.KubeletServer{ - KubeletFlags: *s.kubeletflags, - KubeletConfiguration: *s.kubeconfig, - } - - kubeletDeps, err := kubelet.UnsecuredDependencies(ctx, kubeletServer, utilfeature.DefaultFeatureGate) - if err != nil { - return fmt.Errorf("error fetching dependencies: %w", err) - } - - errc := make(chan error) - - // Run healthcheck probe and kubelet in parallel. - // No matter which ends first - if it ends with an error, - // it'll cause ServiceManager to trigger graceful shutdown. - - // run readiness check - go func() { - // This endpoint does not use TLS, but reusing the same function without verification. - healthcheckStatus := util.RetryInsecureGet(ctx, "http://localhost:10248/healthz") - if healthcheckStatus != 200 { - e := fmt.Errorf("%s failed to start", s.Name()) - klog.Error(e) - errc <- e - return - } - klog.Infof("%s is ready", s.Name()) - close(ready) - }() - - panicChannel := make(chan any, 1) - go func() { - defer func() { - if r := recover(); r != nil { - panicChannel <- r - } - }() - errc <- kubelet.Run(ctx, kubeletServer, kubeletDeps, utilfeature.DefaultFeatureGate) - }() - - select { - case err := <-errc: - return err - case perr := <-panicChannel: - panic(perr) - } -} - -func loadConfigFile(name string) (*kubeletconfig.KubeletConfiguration, error) { +func loadConfigFile(ctx context.Context, name string) (*kubeletconfig.KubeletConfiguration, error) { const errFmt = "failed to load Kubelet config file %s, error %v" // compute absolute path based on current working dir kubeletConfigFile, err := filepath.Abs(name) @@ -227,7 +230,7 @@ func loadConfigFile(name string) (*kubeletconfig.KubeletConfiguration, error) { if err != nil { return nil, fmt.Errorf(errFmt, name, err) } - kc, err := loader.Load(context.TODO()) + kc, err := loader.Load(ctx) if err != nil { return nil, fmt.Errorf(errFmt, name, err) } diff --git a/pkg/node/kubelet_test.go b/pkg/node/kubelet_test.go index a98a071c0f..0cea9665de 100644 --- a/pkg/node/kubelet_test.go +++ b/pkg/node/kubelet_test.go @@ -43,8 +43,7 @@ reservedMemory: memory: 1100Mi numaNode: 0` - kubelet := &KubeletServer{} - data, err := kubelet.generateConfig(cfg) + data, err := generateConfig(cfg) assert.NoError(t, err) assert.Contains(t, string(data), expectedConfigPart) } From 6a5c66db5e4af3987377044bcc9deedd4c55ed48 Mon Sep 17 00:00:00 2001 From: Pablo Acevedo Montserrat Date: Fri, 31 Oct 2025 12:18:30 +0100 Subject: [PATCH 4/8] OCPEDGE-2038: Propagate context in promote-learner --- pkg/cmd/promotelearner.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/promotelearner.go b/pkg/cmd/promotelearner.go index 643686746d..b6004516eb 100644 --- a/pkg/cmd/promotelearner.go +++ b/pkg/cmd/promotelearner.go @@ -54,7 +54,7 @@ func runPromoteLearner(ctx context.Context) error { } klog.Info("Promoting etcd learner to member") - if err := promoteEtcdLearner(etcdMembers, cfg); err != nil { + if err := promoteEtcdLearner(ctx, etcdMembers, cfg); err != nil { return fmt.Errorf("failed to promote etcd learner: %w", err) } @@ -66,7 +66,7 @@ func runPromoteLearner(ctx context.Context) error { return nil } -func promoteEtcdLearner(clusterMembers []string, cfg *config.Config) error { +func promoteEtcdLearner(ctx context.Context, clusterMembers []string, cfg *config.Config) error { certsDir := cryptomaterial.CertsDirectory(config.DataDir) etcdPeerClientCertDir := cryptomaterial.EtcdPeerCertDir(certsDir) @@ -92,14 +92,14 @@ func promoteEtcdLearner(clusterMembers []string, cfg *config.Config) error { Endpoints: endpoints, DialTimeout: 5 * time.Second, TLS: tlsConfig, - Context: context.Background(), + Context: ctx, }) if err != nil { return fmt.Errorf("failed to create etcd client: %v", err) } defer client.Close() - memberResponse, err := client.MemberList(context.Background()) + memberResponse, err := client.MemberList(ctx) if err != nil { return fmt.Errorf("failed to list etcd members: %v", err) } @@ -124,7 +124,7 @@ func promoteEtcdLearner(clusterMembers []string, cfg *config.Config) error { return fmt.Errorf("node %s is not a learner", cfg.CanonicalNodeName()) } - response, err := client.MemberPromote(context.Background(), id) + response, err := client.MemberPromote(ctx, id) if err != nil { return fmt.Errorf("failed to promote etcd learner: %v", err) } From 67b18ad15fde63d7e65d7b6b5c7affcd65bfb863 Mon Sep 17 00:00:00 2001 From: Pablo Acevedo Montserrat Date: Thu, 6 Nov 2025 12:52:10 +0100 Subject: [PATCH 5/8] OCPEDGE-2038: Remove auto restart of microshift after promotion --- pkg/cmd/promotelearner.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/promotelearner.go b/pkg/cmd/promotelearner.go index b6004516eb..958d3e4d4e 100644 --- a/pkg/cmd/promotelearner.go +++ b/pkg/cmd/promotelearner.go @@ -58,11 +58,8 @@ func runPromoteLearner(ctx context.Context) error { return fmt.Errorf("failed to promote etcd learner: %w", err) } - klog.Info("Restarting MicroShift service") - if err := restartMicroShift(); err != nil { - return fmt.Errorf("failed to restart MicroShift service: %w", err) - } - klog.Info("Learner promotion process completed successfully") + klog.Info("etcd node successfully promoted to member. Restart MicroShift service") + return nil } From f974d5617fdce05fd7d9e36697e74b7a26fdd029 Mon Sep 17 00:00:00 2001 From: Pablo Acevedo Montserrat Date: Thu, 6 Nov 2025 23:45:29 +0100 Subject: [PATCH 6/8] OCPEDGE-2038: Add etcd client helper function --- pkg/controllers/etcd.go | 84 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/pkg/controllers/etcd.go b/pkg/controllers/etcd.go index e5631c6173..0ce524141e 100644 --- a/pkg/controllers/etcd.go +++ b/pkg/controllers/etcd.go @@ -19,6 +19,7 @@ import ( "context" "errors" "fmt" + "net" "os" "os/exec" "path/filepath" @@ -26,6 +27,10 @@ import ( "github.com/openshift/microshift/pkg/config" "github.com/openshift/microshift/pkg/util/cryptomaterial" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" klog "k8s.io/klog/v2" "go.etcd.io/etcd/client/pkg/v3/transport" @@ -251,3 +256,82 @@ func getEtcdClient(ctx context.Context) (*clientv3.Client, error) { } return cli, nil } + +// GetClusterEtcdClient creates a new etcd client for the cluster. +// It uses the kubeconfig to list the nodes in the cluster to test which ones are learners +// and then creates a new client with voting members only. +func GetClusterEtcdClient(ctx context.Context, kubeConfigPath string) (*clientv3.Client, error) { + certsDir := cryptomaterial.CertsDirectory(config.DataDir) + etcdAPIServerClientCertDir := cryptomaterial.EtcdAPIServerClientCertDir(certsDir) + + tlsInfo := transport.TLSInfo{ + CertFile: cryptomaterial.ClientCertPath(etcdAPIServerClientCertDir), + KeyFile: cryptomaterial.ClientKeyPath(etcdAPIServerClientCertDir), + TrustedCAFile: cryptomaterial.CACertPath(cryptomaterial.EtcdSignerDir(certsDir)), + } + tlsConfig, err := tlsInfo.ClientConfig() + if err != nil { + return nil, err + } + + client, err := clientv3.New(clientv3.Config{ + Endpoints: []string{"https://localhost:2379"}, + DialTimeout: 5 * time.Second, + TLS: tlsConfig, + Context: ctx, + }) + if err != nil { + return nil, err + } + defer func() { _ = client.Close() }() + + restConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath) + if err != nil { + return nil, fmt.Errorf("failed to load kubeconfig from %s: %v", kubeConfigPath, err) + } + adminClient, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return nil, fmt.Errorf("failed to create admin kubernetes client: %w", err) + } + + nodes, err := adminClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to list nodes: %w", err) + } + + var memberEndpoints []string + for _, node := range nodes.Items { + var nodeIP string + for _, addr := range node.Status.Addresses { + if addr.Type == corev1.NodeInternalIP { + nodeIP = addr.Address + break + } + } + if nodeIP == "" { + continue + } + endpoint := net.JoinHostPort(nodeIP, "2379") + status, err := client.Status(ctx, endpoint) + if err != nil { + continue + } + if status != nil && !status.IsLearner { + memberEndpoints = append(memberEndpoints, fmt.Sprintf("https://%s", endpoint)) + } + } + if len(memberEndpoints) == 0 { + memberEndpoints = []string{"https://localhost:2379"} + } + + clusterClient, err := clientv3.New(clientv3.Config{ + Endpoints: memberEndpoints, + DialTimeout: 5 * time.Second, + TLS: tlsConfig, + Context: ctx, + }) + if err != nil { + return nil, err + } + return clusterClient, nil +} From 993e79360392b864ac9e723d205a11743aaa1bb0 Mon Sep 17 00:00:00 2001 From: Pablo Acevedo Montserrat Date: Thu, 6 Nov 2025 23:46:07 +0100 Subject: [PATCH 7/8] OCPEDGE-2038: Rework etcd client logic in promote-learner --- pkg/cmd/promotelearner.go | 52 ++++----------------------------------- 1 file changed, 5 insertions(+), 47 deletions(-) diff --git a/pkg/cmd/promotelearner.go b/pkg/cmd/promotelearner.go index 958d3e4d4e..f15328be4e 100644 --- a/pkg/cmd/promotelearner.go +++ b/pkg/cmd/promotelearner.go @@ -3,14 +3,10 @@ package cmd import ( "context" "fmt" - "strings" - "time" "github.com/openshift/microshift/pkg/config" - "github.com/openshift/microshift/pkg/util/cryptomaterial" + "github.com/openshift/microshift/pkg/controllers" "github.com/spf13/cobra" - "go.etcd.io/etcd/client/pkg/v3/transport" - clientv3 "go.etcd.io/etcd/client/v3" "k8s.io/klog/v2" ) @@ -42,19 +38,8 @@ func runPromoteLearner(ctx context.Context) error { return fmt.Errorf("failed to load MicroShift configuration: %w", err) } - klog.Infof("Creating Kubernetes client from %s", cfg.KubeConfigPath(config.KubeAdmin)) - client, err := createKubernetesClient(cfg.KubeConfigPath(config.KubeAdmin)) - if err != nil { - return fmt.Errorf("failed to create Kubernetes client: %w", err) - } - - etcdMembers, err := getEtcdClusterNodes(ctx, client) - if err != nil { - return fmt.Errorf("failed to get cluster information: %w", err) - } - klog.Info("Promoting etcd learner to member") - if err := promoteEtcdLearner(ctx, etcdMembers, cfg); err != nil { + if err := promoteEtcdLearner(ctx, cfg); err != nil { return fmt.Errorf("failed to promote etcd learner: %w", err) } @@ -63,38 +48,12 @@ func runPromoteLearner(ctx context.Context) error { return nil } -func promoteEtcdLearner(ctx context.Context, clusterMembers []string, cfg *config.Config) error { - certsDir := cryptomaterial.CertsDirectory(config.DataDir) - etcdPeerClientCertDir := cryptomaterial.EtcdPeerCertDir(certsDir) - - tlsInfo := transport.TLSInfo{ - CertFile: cryptomaterial.PeerCertPath(etcdPeerClientCertDir), - KeyFile: cryptomaterial.PeerKeyPath(etcdPeerClientCertDir), - TrustedCAFile: cryptomaterial.CACertPath(cryptomaterial.EtcdSignerDir(certsDir)), - } - tlsConfig, err := tlsInfo.ClientConfig() - if err != nil { - return fmt.Errorf("failed to create etcd client TLS config: %v", err) - } - var endpoints []string - for _, member := range clusterMembers { - parts := strings.SplitN(member, "=", 2) - if len(parts) == 2 { - endpoint := strings.Replace(parts[1], ":2380", ":2379", 1) - endpoints = append(endpoints, endpoint) - } - } - - client, err := clientv3.New(clientv3.Config{ - Endpoints: endpoints, - DialTimeout: 5 * time.Second, - TLS: tlsConfig, - Context: ctx, - }) +func promoteEtcdLearner(ctx context.Context, cfg *config.Config) error { + client, err := controllers.GetClusterEtcdClient(ctx, cfg.KubeConfigPath(config.KubeAdmin)) if err != nil { return fmt.Errorf("failed to create etcd client: %v", err) } - defer client.Close() + defer func() { _ = client.Close() }() memberResponse, err := client.MemberList(ctx) if err != nil { @@ -126,6 +85,5 @@ func promoteEtcdLearner(ctx context.Context, clusterMembers []string, cfg *confi return fmt.Errorf("failed to promote etcd learner: %v", err) } klog.Infof("Successfully promoted etcd learner %s with response: %v", cfg.CanonicalNodeName(), response) - return nil } From 98742106d535fa89498998012bab13c32ec44aa6 Mon Sep 17 00:00:00 2001 From: Pablo Acevedo Montserrat Date: Fri, 7 Nov 2025 08:38:28 +0100 Subject: [PATCH 8/8] OCPEDGE-2038: Hide command in downstream build --- pkg/cmd/promotelearner.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/cmd/promotelearner.go b/pkg/cmd/promotelearner.go index f15328be4e..4eb79ab4e2 100644 --- a/pkg/cmd/promotelearner.go +++ b/pkg/cmd/promotelearner.go @@ -6,6 +6,7 @@ import ( "github.com/openshift/microshift/pkg/config" "github.com/openshift/microshift/pkg/controllers" + "github.com/openshift/microshift/pkg/version" "github.com/spf13/cobra" "k8s.io/klog/v2" @@ -27,6 +28,11 @@ Use this command only after the learner node has fully caught up with the cluste return runPromoteLearner(cmd.Context()) }, } + + if version.Get().BuildVariant != version.BuildVariantCommunity { + cmd.Hidden = true + } + return cmd }