Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3ddb659
kubeadm: skip promote call when etcd member is already a voting member
wgkingk Apr 15, 2026
720f13b
test/compatibility_lifecycle: resolve feature names from variables
siyuanfoundation Apr 24, 2026
746956b
flowcontrol: emit Required when spec.type or limitResponse.type is empty
yongruilin May 1, 2026
bed40bd
flowcontrol: cover Required rule for spec.type and limitResponse.type
yongruilin May 1, 2026
ff173bd
Fix job controller reporting active=0 during pod creation backoff
akhilsingh-git Jun 3, 2026
903e7fc
Update CHANGELOG/CHANGELOG-1.36.md for v1.36.2
k8s-release-robot Jun 11, 2026
d20c60a
Align DeviceTaintRule informer API version with handlers
nojnhuh Jun 11, 2026
24b3a25
kubeadm: use KubernetesAPICallTimeout for mandatory kubeadm-config fetch
damdo Jun 17, 2026
1a4d068
Merge pull request #139808 from damdo/automated-cherry-pick-of-139667…
k8s-ci-robot Jun 17, 2026
5acf40f
kubeadm: treat already promoted learner as successful
jihyun-huh Jun 18, 2026
b86d94a
Restore string JSON encoding of cri-api KeyValue
liggitt Jun 23, 2026
ab57dc0
Make utf8 replacement char test pass on Go 1.27
liggitt Jun 24, 2026
ccca5a9
kubelet startPodSync: reuse the previous context to fix memory leak r…
compumike Jun 18, 2026
9f5059c
Merge pull request #139965 from liggitt/automated-cherry-pick-of-#139…
kubernetes-prow[bot] Jul 1, 2026
f127ce3
Merge pull request #139642 from akhilsingh-git/automated-cherry-pick-…
kubernetes-prow[bot] Jul 3, 2026
5f28948
Merge pull request #139910 from jihyun-huh/automated-cherry-pick-of-#…
kubernetes-prow[bot] Jul 3, 2026
7e33c3d
Merge pull request #138974 from stmcginnis/automated-cherry-pick-of-#…
kubernetes-prow[bot] Jul 3, 2026
115636a
Merge pull request #139366 from yongruilin/automated-cherry-pick-of-#…
kubernetes-prow[bot] Jul 3, 2026
6ddb28b
Merge pull request #139681 from nojnhuh/automated-cherry-pick-of-#139…
kubernetes-prow[bot] Jul 3, 2026
fc22179
Merge pull request #140066 from compumike/automated-cherry-pick-of-#1…
kubernetes-prow[bot] Jul 3, 2026
c9ee548
Merge pull request #138493 from wgkingk/automated-cherry-pick-of-#138…
kubernetes-prow[bot] Jul 3, 2026
f015292
Bump sigs.k8s.io/structured-merge-diff/v6 to v6.3.3
jpbetz Jul 7, 2026
4434901
Add e2e test for setting maps and slices to null via SSA
jpbetz Jul 2, 2026
fb2467e
stop logging missing optional container annotations
HirazawaUi Jul 1, 2026
2c14a99
Bump images and versions to golang 1.26.5 and update distroless-iptables
palnabarun Jul 15, 2026
5874ee7
DRA: roll back reserved state in allocateDevice
thc1006 Jul 15, 2026
9419ebe
Merge pull request #140296 from jpbetz/cherry-pick-smd-306-revert
kubernetes-prow[bot] Jul 18, 2026
dec683b
Merge pull request #140322 from HirazawaUi/automated-cherry-pick-of-#…
kubernetes-prow[bot] Jul 18, 2026
cc4f7df
Merge pull request #140663 from thc1006/dra/release-reserved-counters…
kubernetes-prow[bot] Jul 18, 2026
8ba6370
Merge pull request #140581 from palnabarun/bump-go-1.36
kubernetes-prow[bot] Jul 20, 2026
0f29094
Release commit for Kubernetes v1.36.3
k8s-release-robot Jul 22, 2026
450e998
Merge tag 'v1.36.3' into rebase-master-v1.36.3
redhat-chai-bot Jul 27, 2026
9deb725
UPSTREAM: <drop>: hack/update-vendor.sh, make update and update image
redhat-chai-bot Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .go-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.26.4
1.26.5
231 changes: 176 additions & 55 deletions CHANGELOG/CHANGELOG-1.36.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/build-image/cross/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v1.36.0-go1.26.4-bullseye.0
v1.36.0-go1.26.5-bullseye.0
4 changes: 2 additions & 2 deletions build/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ readonly REMOTE_OUTPUT_BINPATH="${REMOTE_OUTPUT_SUBPATH}/bin"
readonly REMOTE_OUTPUT_GOPATH="${REMOTE_OUTPUT_SUBPATH}/go"

# These are the default versions (image tags) for their respective base images.
readonly __default_distroless_iptables_version=v0.9.3
readonly __default_go_runner_version=v2.4.0-go1.26.4-bookworm.0
readonly __default_distroless_iptables_version=v0.9.6
readonly __default_go_runner_version=v2.4.0-go1.26.5-bookworm.0
readonly __default_setcap_version=bookworm-v1.0.6

# The default image for all binaries which are dynamically linked.
Expand Down
6 changes: 3 additions & 3 deletions build/dependencies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ dependencies:
# should also be updated, but go-runner is much harder to exploit and has
# far less relevancy to go updates for Kubernetes more generally.
- name: "registry.k8s.io/kube-cross: dependents"
version: v1.36.0-go1.26.4-bullseye.0
version: v1.36.0-go1.26.5-bullseye.0
refPaths:
- path: build/build-image/cross/VERSION

Expand Down Expand Up @@ -175,15 +175,15 @@ dependencies:
match: registry\.k8s\.io\/build-image\/debian-base:[a-zA-Z]+\-v((([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)

- name: "registry.k8s.io/distroless-iptables: dependents"
version: v0.9.3
version: v0.9.6
refPaths:
- path: build/common.sh
match: __default_distroless_iptables_version=
- path: test/utils/image/manifest.go
match: configs\[DistrolessIptables\] = Config{list\.BuildImageRegistry, "distroless-iptables", "v([0-9]+)\.([0-9]+)\.([0-9]+)"}

- name: "registry.k8s.io/go-runner: dependents"
version: v2.4.0-go1.26.4-bookworm.0
version: v2.4.0-go1.26.5-bookworm.0
refPaths:
- path: build/common.sh
match: __default_go_runner_version=
Expand Down
2 changes: 1 addition & 1 deletion cmd/kubeadm/app/cmd/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ func getInternalCfg(cfgPath string, client kubernetes.Interface, cfg kubeadmapiv
getNodeRegistration := true
getAPIEndpoint := staticpodutil.IsControlPlaneNode()
getComponentConfigs := true
internalcfg, err := configutil.FetchInitConfigurationFromCluster(client, printer, logPrefix, getNodeRegistration, getAPIEndpoint, getComponentConfigs)
internalcfg, err := configutil.FetchInitConfigurationFromCluster(client, printer, logPrefix, getNodeRegistration, getAPIEndpoint, getComponentConfigs, true)
if err == nil {
printer.Println() // add empty line to separate the FetchInitConfigurationFromCluster output from the command output
// certificate renewal or expiration checking doesn't depend on a running cluster, which means the CertificatesDir
Expand Down
2 changes: 1 addition & 1 deletion cmd/kubeadm/app/cmd/join.go
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ func fetchInitConfiguration(client clientset.Interface) (*kubeadmapi.InitConfigu
getNodeRegistration := false
getAPIEndpoint := false
getComponentConfigs := true
initConfiguration, err := configutil.FetchInitConfigurationFromCluster(client, nil, "preflight", getNodeRegistration, getAPIEndpoint, getComponentConfigs)
initConfiguration, err := configutil.FetchInitConfigurationFromCluster(client, nil, "preflight", getNodeRegistration, getAPIEndpoint, getComponentConfigs, false)
if err != nil {
return nil, errors.Wrap(err, "unable to fetch the kubeadm-config ConfigMap")
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/kubeadm/app/cmd/reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func newResetData(cmd *cobra.Command, opts *resetOptions, in io.Reader, out io.W
getNodeRegistration := true
getAPIEndpoint := staticpodutil.IsControlPlaneNode()
getComponentConfigs := true
initCfg, err = configutil.FetchInitConfigurationFromCluster(client, nil, "reset", getNodeRegistration, getAPIEndpoint, getComponentConfigs)
initCfg, err = configutil.FetchInitConfigurationFromCluster(client, nil, "reset", getNodeRegistration, getAPIEndpoint, getComponentConfigs, true)
if err != nil {
klog.Warningf("[reset] Unable to fetch the kubeadm-config ConfigMap from cluster: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/kubeadm/app/cmd/upgrade/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ func newApplyData(cmd *cobra.Command, args []string, applyFlags *applyFlags) (*a
getNodeRegistration := true
isControlPlaneNode := true
getComponentConfigs := true
initCfg, err := configutil.FetchInitConfigurationFromCluster(client, nil, "upgrade", getNodeRegistration, isControlPlaneNode, getComponentConfigs)
initCfg, err := configutil.FetchInitConfigurationFromCluster(client, nil, "upgrade", getNodeRegistration, isControlPlaneNode, getComponentConfigs, false)
if err != nil {
if apierrors.IsNotFound(err) {
_, _ = printer.Printf("[upgrade] In order to upgrade, a ConfigMap called %q in the %q namespace must exist.\n", constants.KubeadmConfigConfigMap, metav1.NamespaceSystem)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kubeadm/app/cmd/upgrade/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func enforceRequirements(flagSet *pflag.FlagSet, flags *applyPlanFlags, args []s
getNodeRegistration := true
getAPIEndpoint := staticpodutil.IsControlPlaneNode()
getComponentConfigs := true
initCfg, err := configutil.FetchInitConfigurationFromCluster(client, printer, "upgrade/config", getNodeRegistration, getAPIEndpoint, getComponentConfigs)
initCfg, err := configutil.FetchInitConfigurationFromCluster(client, printer, "upgrade/config", getNodeRegistration, getAPIEndpoint, getComponentConfigs, false)
if err != nil {
return nil, nil, nil, nil, errors.Wrap(err, "[upgrade/init config] FATAL")
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/kubeadm/app/cmd/upgrade/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func validateManifestsPath(manifests ...string) (err error) {
}

// FetchInitConfigurationFunc defines the signature of the function which will fetch InitConfiguration from cluster.
type FetchInitConfigurationFunc func(client clientset.Interface, printer output.Printer, logPrefix string, getNodeRegistration, getAPIEndpoint, getComponentConfigs bool) (*kubeadmapi.InitConfiguration, error)
type FetchInitConfigurationFunc func(client clientset.Interface, printer output.Printer, logPrefix string, getNodeRegistration, getAPIEndpoint, getComponentConfigs, shortConfigMapGet bool) (*kubeadmapi.InitConfiguration, error)

func runDiff(fs *pflag.FlagSet, flags *diffFlags, args []string, fetchInitConfigurationFromCluster FetchInitConfigurationFunc) error {
externalCfg := &v1beta4.UpgradeConfiguration{}
Expand All @@ -123,7 +123,7 @@ func runDiff(fs *pflag.FlagSet, flags *diffFlags, args []string, fetchInitConfig
getNodeRegistration := true
getAPIEndpoint := staticpodutil.IsControlPlaneNode()
getComponentConfigs := false
initCfg, err := fetchInitConfigurationFromCluster(client, &output.TextPrinter{}, "upgrade/diff", getNodeRegistration, getAPIEndpoint, getComponentConfigs)
initCfg, err := fetchInitConfigurationFromCluster(client, &output.TextPrinter{}, "upgrade/diff", getNodeRegistration, getAPIEndpoint, getComponentConfigs, false)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/kubeadm/app/cmd/upgrade/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func createTestRunDiffFile(contents []byte) (string, error) {
return file.Name(), nil
}

func fakeFetchInitConfig(client clientset.Interface, printer output.Printer, logPrefix string, getNodeRegistration, getAPIEndpoint, getComponentConfigs bool) (*kubeadmapi.InitConfiguration, error) {
func fakeFetchInitConfig(client clientset.Interface, printer output.Printer, logPrefix string, getNodeRegistration, getAPIEndpoint, getComponentConfigs, shortConfigMapGet bool) (*kubeadmapi.InitConfiguration, error) {
return &kubeadmapi.InitConfiguration{
ClusterConfiguration: kubeadmapi.ClusterConfiguration{
KubernetesVersion: "v1.0.1",
Expand Down
2 changes: 1 addition & 1 deletion cmd/kubeadm/app/cmd/upgrade/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func newNodeData(cmd *cobra.Command, nodeOptions *nodeOptions, out io.Writer) (*
getNodeRegistration := true
getAPIEndpoint := isControlPlaneNode
getComponentConfigs := true
initCfg, err := configutil.FetchInitConfigurationFromCluster(client, nil, "upgrade", getNodeRegistration, getAPIEndpoint, getComponentConfigs)
initCfg, err := configutil.FetchInitConfigurationFromCluster(client, nil, "upgrade", getNodeRegistration, getAPIEndpoint, getComponentConfigs, false)
if err != nil {
return nil, errors.Wrap(err, "unable to fetch the kubeadm-config ConfigMap")
}
Expand Down
35 changes: 30 additions & 5 deletions cmd/kubeadm/app/util/config/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"time"

authv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
Expand All @@ -51,8 +52,10 @@ import (
kubeadmruntime "k8s.io/kubernetes/cmd/kubeadm/app/util/runtime"
)

// FetchInitConfigurationFromCluster fetches configuration from a ConfigMap in the cluster
func FetchInitConfigurationFromCluster(client clientset.Interface, printer output.Printer, logPrefix string, getNodeRegistration, getAPIEndpoint, getComponentConfigs bool) (*kubeadmapi.InitConfiguration, error) {
// FetchInitConfigurationFromCluster fetches configuration from a ConfigMap in the cluster.
// If shortConfigMapGet is true, a short retry is used when fetching the kubeadm-config ConfigMap,
// which is suitable for callers like "kubeadm reset" that don't need a long retry.
func FetchInitConfigurationFromCluster(client clientset.Interface, printer output.Printer, logPrefix string, getNodeRegistration, getAPIEndpoint, getComponentConfigs, shortConfigMapGet bool) (*kubeadmapi.InitConfiguration, error) {
if printer == nil {
printer = &output.TextPrinter{}
}
Expand All @@ -61,7 +64,7 @@ func FetchInitConfigurationFromCluster(client clientset.Interface, printer outpu
_, _ = printer.Printf("[%s] Use 'kubeadm init phase upload-config kubeadm --config your-config-file' to re-upload it.\n", logPrefix)

// Fetch the actual config from cluster
cfg, err := getInitConfigurationFromCluster(constants.KubernetesDir, client, getNodeRegistration, getAPIEndpoint, getComponentConfigs)
cfg, err := getInitConfigurationFromCluster(constants.KubernetesDir, client, getNodeRegistration, getAPIEndpoint, getComponentConfigs, shortConfigMapGet)
if err != nil {
return nil, err
}
Expand All @@ -78,9 +81,31 @@ func FetchInitConfigurationFromCluster(client clientset.Interface, printer outpu
}

// getInitConfigurationFromCluster is separate only for testing purposes, don't call it directly, use FetchInitConfigurationFromCluster instead
func getInitConfigurationFromCluster(kubeconfigDir string, client clientset.Interface, getNodeRegistration, getAPIEndpoint, getComponentConfigs bool) (*kubeadmapi.InitConfiguration, error) {
func getInitConfigurationFromCluster(kubeconfigDir string, client clientset.Interface, getNodeRegistration, getAPIEndpoint, getComponentConfigs, shortConfigMapGet bool) (*kubeadmapi.InitConfiguration, error) {
// Also, the config map really should be KubeadmConfigConfigMap...
configMap, err := apiclient.GetConfigMapWithShortRetry(client, metav1.NamespaceSystem, constants.KubeadmConfigConfigMap)
var configMap *v1.ConfigMap
var err error
if shortConfigMapGet {
configMap, err = apiclient.GetConfigMapWithShortRetry(client, metav1.NamespaceSystem, constants.KubeadmConfigConfigMap)
} else {
var lastErr error
err = wait.PollUntilContextTimeout(context.Background(),
constants.KubernetesAPICallRetryInterval,
kubeadmapi.GetActiveTimeouts().KubernetesAPICall.Duration,
true, func(_ context.Context) (bool, error) {
var err error
configMap, err = client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(
context.Background(), constants.KubeadmConfigConfigMap, metav1.GetOptions{})
if err == nil {
return true, nil
}
lastErr = err
return false, nil
})
if err != nil {
err = lastErr
}
Comment on lines +92 to +107

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Propagate the poll context and guard against a nil lastErr.

The closure discards the poll context and issues the Get with context.Background(), so individual API calls aren't bounded by the KubernetesAPICall deadline. Also, if wait ever returns an error while lastErr is nil, err becomes nil and execution falls through to line 122 with a nil configMap.

As per path instructions, "context.Context for cancellation and timeouts".

🛠️ Proposed fix
-		err = wait.PollUntilContextTimeout(context.Background(),
+		err = wait.PollUntilContextTimeout(context.Background(),
 			constants.KubernetesAPICallRetryInterval,
 			kubeadmapi.GetActiveTimeouts().KubernetesAPICall.Duration,
-			true, func(_ context.Context) (bool, error) {
+			true, func(ctx context.Context) (bool, error) {
 				var err error
 				configMap, err = client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(
-					context.Background(), constants.KubeadmConfigConfigMap, metav1.GetOptions{})
+					ctx, constants.KubeadmConfigConfigMap, metav1.GetOptions{})
 				if err == nil {
 					return true, nil
 				}
 				lastErr = err
 				return false, nil
 			})
-		if err != nil {
+		if err != nil && lastErr != nil {
 			err = lastErr
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
err = wait.PollUntilContextTimeout(context.Background(),
constants.KubernetesAPICallRetryInterval,
kubeadmapi.GetActiveTimeouts().KubernetesAPICall.Duration,
true, func(_ context.Context) (bool, error) {
var err error
configMap, err = client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(
context.Background(), constants.KubeadmConfigConfigMap, metav1.GetOptions{})
if err == nil {
return true, nil
}
lastErr = err
return false, nil
})
if err != nil {
err = lastErr
}
err = wait.PollUntilContextTimeout(context.Background(),
constants.KubernetesAPICallRetryInterval,
kubeadmapi.GetActiveTimeouts().KubernetesAPICall.Duration,
true, func(ctx context.Context) (bool, error) {
var err error
configMap, err = client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(
ctx, constants.KubeadmConfigConfigMap, metav1.GetOptions{})
if err == nil {
return true, nil
}
lastErr = err
return false, nil
})
if err != nil && lastErr != nil {
err = lastErr
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/kubeadm/app/util/config/cluster.go` around lines 92 - 107, Update the
polling closure in the cluster configuration retrieval flow to accept and pass
its poll context to the ConfigMaps Get call instead of using
context.Background(), preserving the KubernetesAPICall timeout. When
wait.PollUntilContextTimeout returns an error, only replace it with lastErr when
lastErr is non-nil; otherwise retain the poll error so execution cannot continue
with a nil configMap.

Source: Path instructions

}
if err != nil {
return nil, errors.Wrap(err, "failed to get config map")
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/kubeadm/app/util/config/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ func TestGetInitConfigurationFromCluster(t *testing.T) {
}

getComponentConfigs := true
cfg, err := getInitConfigurationFromCluster(tmpdir, client, rt.getNodeRegistration, rt.getAPIEndpoint, getComponentConfigs)
cfg, err := getInitConfigurationFromCluster(tmpdir, client, rt.getNodeRegistration, rt.getAPIEndpoint, getComponentConfigs, true)
if rt.expectedError != (err != nil) {
t.Errorf("unexpected return err from getInitConfigurationFromCluster: %v", err)
return
Expand Down
42 changes: 37 additions & 5 deletions cmd/kubeadm/app/util/etcd/etcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,14 +584,17 @@ func (c *Client) getMemberStatus(memberID uint64) (isLearner bool, started bool,
func (c *Client) MemberPromote(learnerID uint64) error {
var (
lastError error
isLearner bool
isStarted bool
learnerIDUint = strconv.FormatUint(learnerID, 16)
)

klog.V(1).Infof("[etcd] Waiting for a learner to start: %s", learnerIDUint)

err := wait.PollUntilContextTimeout(context.Background(), constants.EtcdAPICallRetryInterval, kubeadmapi.GetActiveTimeouts().EtcdAPICall.Duration,
true, func(_ context.Context) (bool, error) {
isLearner, started, err := c.getMemberStatus(learnerID)
var err error
isLearner, isStarted, err = c.getMemberStatus(learnerID)
if err != nil {
lastError = errors.WithMessagef(err, "failed to get member %s status", learnerIDUint)
return false, nil
Expand All @@ -600,7 +603,7 @@ func (c *Client) MemberPromote(learnerID uint64) error {
klog.V(1).Infof("[etcd] Member %s was already promoted.", learnerIDUint)
return true, nil
}
if !started {
if !isStarted {
klog.V(1).Infof("[etcd] Member %s is not started yet. Waiting for it to be started.", learnerIDUint)
lastError = errors.Errorf("the etcd member %s is not started", learnerIDUint)
return false, nil
Expand All @@ -611,6 +614,10 @@ func (c *Client) MemberPromote(learnerID uint64) error {
return lastError
}

if !isLearner {
return nil
}
Comment on lines +617 to +619

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Early return skips endpoint registration, unlike the equivalent branch in the promote loop.

When the member is already voting on the first status check, MemberPromote returns before reaching the addEndpoint loop at Line 679. The analogous "already a voting member" case inside the retry loop (Lines 655-659) does populate memberList and register the endpoint, so the outcome now depends on when the promotion was observed. Consider making both paths add the endpoint.

🛠️ Sketch
 	if !isLearner {
-		return nil
+		resp, err := c.listMembersOnce()
+		if err != nil {
+			return err
+		}
+		c.addPeerEndpoint(resp.Members, learnerID)
+		return nil
 	}

(factor the Line 679 loop into a small helper reused by both paths)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !isLearner {
return nil
}
if !isLearner {
resp, err := c.listMembersOnce()
if err != nil {
return err
}
c.addPeerEndpoint(resp.Members, learnerID)
return nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/kubeadm/app/util/etcd/etcd.go` around lines 617 - 619, Update
MemberPromote so the initial !isLearner early-return path also adds the member
to memberList and registers its endpoint, matching the already-voting branch in
the retry loop. Factor the existing endpoint-registration loop near addEndpoint
into a helper and invoke it from both paths, preserving the current behavior for
learner promotion.


klog.V(1).Infof("[etcd] Promoting a learner as a voting member: %s", learnerIDUint)

cli, err := c.newEtcdClient(c.Endpoints)
Expand All @@ -626,14 +633,39 @@ func (c *Client) MemberPromote(learnerID uint64) error {
// 2. context deadline exceeded
// 3. peer URLs already exists
// Once the client provides a way to check if the etcd learner is ready to promote, the retry logic can be revisited.
var promoteResp *clientv3.MemberPromoteResponse
var memberList []*etcdserverpb.Member
err = wait.PollUntilContextTimeout(context.Background(), constants.EtcdAPICallRetryInterval, kubeadmapi.GetActiveTimeouts().EtcdAPICall.Duration,
true, func(_ context.Context) (bool, error) {
// MemberPromote can return a transient client-side error even if the
// promotion already succeeded on the etcd side. Check the current
// member state before attempting another promotion so that retries
// remain idempotent.
resp, statusErr := c.listMembersOnce()
if statusErr != nil {
klog.V(5).Infof("[etcd] Failed to list members before promoting learner %s: %v", learnerIDUint, statusErr)
lastError = statusErr
return false, nil
}

for _, m := range resp.Members {
if m.ID != learnerID {
continue
}

if !m.IsLearner {
klog.V(1).Infof("[etcd] Member %s is already a voting member, treating promotion as successful", learnerIDUint)
memberList = resp.Members
return true, nil
}
break
}

ctx, cancel := context.WithTimeout(context.Background(), etcdTimeout)
defer cancel()
promoteResp, err = cli.MemberPromote(ctx, learnerID)
promoteResp, err := cli.MemberPromote(ctx, learnerID)
if err == nil {
klog.V(1).Infof("[etcd] The learner was promoted as a voting member: %s", learnerIDUint)
memberList = promoteResp.Members
return true, nil
}
klog.V(5).Infof("[etcd] Promoting the learner %s failed: %v", learnerIDUint, err)
Expand All @@ -644,7 +676,7 @@ func (c *Client) MemberPromote(learnerID uint64) error {
return lastError
}

for _, m := range promoteResp.Members {
for _, m := range memberList {
if m.ID == learnerID {
parsedPeerAddrs, err := url.Parse(m.PeerURLs[0])
if err != nil {
Expand Down
Loading