Skip to content

Commit ffe2e55

Browse files
Implement k0s etcd member-update command
Signed-off-by: Danil-Grigorev <[email protected]>
1 parent 3310d8c commit ffe2e55

File tree

4 files changed

+157
-0
lines changed

4 files changed

+157
-0
lines changed

cmd/etcd/etcd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func NewEtcdCmd() *cobra.Command {
4949
pflags.AddFlagSet(config.GetPersistentFlagSet())
5050

5151
cmd.AddCommand(etcdLeaveCmd())
52+
cmd.AddCommand(etcdUpdateCmd())
5253
cmd.AddCommand(etcdListCmd())
5354

5455
return cmd

cmd/etcd/update.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// SPDX-FileCopyrightText: 2025 k0s authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package etcd
5+
6+
import (
7+
"cmp"
8+
"fmt"
9+
"net"
10+
"net/url"
11+
"strconv"
12+
13+
"github.com/k0sproject/k0s/pkg/config"
14+
"github.com/k0sproject/k0s/pkg/etcd"
15+
16+
"github.com/sirupsen/logrus"
17+
"github.com/spf13/cobra"
18+
"github.com/spf13/pflag"
19+
)
20+
21+
func etcdUpdateCmd() *cobra.Command {
22+
var peerAddressArg string
23+
var memberName string
24+
25+
cmd := &cobra.Command{
26+
Use: "member-update",
27+
Short: "Update specific member of the cluster",
28+
// accept peer address list as the first flag
29+
Args: cobra.MinimumNArgs(1),
30+
RunE: func(cmd *cobra.Command, peerAddr []string) error {
31+
opts, err := config.GetCmdOpts(cmd)
32+
if err != nil {
33+
return err
34+
}
35+
nodeConfig, err := opts.K0sVars.NodeConfig()
36+
if err != nil {
37+
return err
38+
}
39+
ctx := cmd.Context()
40+
41+
peerAddress := cmp.Or(peerAddressArg, nodeConfig.Spec.Storage.Etcd.PeerAddress)
42+
if memberName == "" && peerAddress == "" {
43+
return fmt.Errorf("can't update member: no member name or peer address specified")
44+
}
45+
46+
etcdClient, err := etcd.NewClient(opts.K0sVars.CertRootDir, opts.K0sVars.EtcdCertDir, nodeConfig.Spec.Storage.Etcd)
47+
if err != nil {
48+
return fmt.Errorf("can't connect to the etcd: %w", err)
49+
}
50+
51+
var peerID uint64
52+
if memberName != "" {
53+
peerID, err = etcdClient.GetPeerIDByName(ctx, memberName)
54+
if err != nil {
55+
logrus.WithField("memberName", memberName).Errorf("Failed to get peer ID")
56+
return err
57+
}
58+
} else if peerAddress != "" {
59+
peerURL := (&url.URL{Scheme: "https", Host: net.JoinHostPort(peerAddress, "2380")}).String()
60+
peerID, err = etcdClient.GetPeerIDByAddress(ctx, peerURL)
61+
if err != nil {
62+
logrus.WithField("peerURL", peerURL).Errorf("Failed to get peer ID")
63+
return err
64+
}
65+
}
66+
67+
if err := etcdClient.UpdateMember(ctx, peerID, peerAddr); err != nil {
68+
logrus.
69+
WithField("peerID", strconv.FormatUint(peerID, 16)).
70+
Errorf("Failed to update cluster member")
71+
return err
72+
}
73+
74+
logrus.
75+
WithField("peerID", strconv.FormatUint(peerID, 16)).
76+
Info("Successfully updated")
77+
return nil
78+
},
79+
}
80+
81+
flags := cmd.Flags()
82+
flags.AddFlagSet(config.GetPersistentFlagSet())
83+
flags.AddFlag(&pflag.Flag{
84+
Name: "peer-address",
85+
Usage: "etcd peer address to update (default <this node's peer address>)",
86+
Value: (*ipOrDNSName)(&peerAddressArg),
87+
})
88+
flags.AddFlag(&pflag.Flag{
89+
Name: "member-name",
90+
Usage: "etcd member name to update",
91+
Value: (*ipOrDNSName)(&memberName),
92+
})
93+
94+
return cmd
95+
}

cmd/etcd/update_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-FileCopyrightText: 2025 k0s authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package etcd
5+
6+
import (
7+
"strings"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestEtcdUpdateCmd(t *testing.T) {
14+
t.Run("requires_minimum_arguments", func(t *testing.T) {
15+
updateCmd := etcdUpdateCmd()
16+
updateCmd.SetArgs([]string{})
17+
err := updateCmd.Execute()
18+
assert.ErrorContains(t, err, "requires at least 1 arg(s)")
19+
})
20+
21+
t.Run("rejects_invalid_peer_address_flag", func(t *testing.T) {
22+
updateCmd := etcdUpdateCmd()
23+
updateCmd.SetArgs([]string{"--peer-address=neither/ip/nor/name", "peer1"})
24+
err := updateCmd.Execute()
25+
assert.ErrorContains(t, err, `invalid argument "neither/ip/nor/name" for "--peer-address" flag: neither an IP address nor a DNS name`)
26+
})
27+
28+
t.Run("rejects_invalid_member_name_flag", func(t *testing.T) {
29+
updateCmd := etcdUpdateCmd()
30+
updateCmd.SetArgs([]string{"--member-name=neither/ip/nor/name", "peer1"})
31+
err := updateCmd.Execute()
32+
assert.ErrorContains(t, err, `invalid argument "neither/ip/nor/name" for "--member-name" flag: neither an IP address nor a DNS name`)
33+
})
34+
35+
t.Run("usage_string_contains_flag_help", func(t *testing.T) {
36+
updateCmd := etcdUpdateCmd()
37+
usageLines := strings.Split(updateCmd.UsageString(), "\n")
38+
assert.Contains(t, usageLines, " --peer-address ip-or-dns-name etcd peer address to update (default <this node's peer address>)")
39+
assert.Contains(t, usageLines, " --member-name ip-or-dns-name etcd member name to update")
40+
})
41+
}

pkg/etcd/client.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,26 @@ func (c *Client) GetPeerIDByAddress(ctx context.Context, peerAddress string) (ui
115115
return 0, fmt.Errorf("peer not found: %s", peerAddress)
116116
}
117117

118+
// GetPeerIDByName looks up peer id by peer name
119+
func (c *Client) GetPeerIDByName(ctx context.Context, peerName string) (uint64, error) {
120+
resp, err := c.client.MemberList(ctx)
121+
if err != nil {
122+
return 0, fmt.Errorf("etcd member list failed: %w", err)
123+
}
124+
for _, m := range resp.Members {
125+
if m.Name == peerName {
126+
return m.ID, nil
127+
}
128+
}
129+
return 0, fmt.Errorf("peer not found: %s", peerName)
130+
}
131+
132+
// UpdateMember updates member by peer ID
133+
func (c *Client) UpdateMember(ctx context.Context, peerID uint64, peerAddr []string) error {
134+
_, err := c.client.MemberUpdate(ctx, peerID, peerAddr)
135+
return err
136+
}
137+
118138
// DeleteMember deletes member by peer name
119139
func (c *Client) DeleteMember(ctx context.Context, peerID uint64) error {
120140
_, err := c.client.MemberRemove(ctx, peerID)

0 commit comments

Comments
 (0)