Skip to content

Commit b32e2f2

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

File tree

5 files changed

+177
-1
lines changed

5 files changed

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

cmd/etcd/update_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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("rejects_invalid_peer_argument", func(t *testing.T) {
15+
updateCmd := etcdUpdateCmd()
16+
updateCmd.SetArgs([]string{"some/path"})
17+
err := updateCmd.Execute()
18+
assert.ErrorContains(t, err, `"some/path" neither an IP address nor a DNS name`)
19+
})
20+
21+
t.Run("requires_minimum_arguments", func(t *testing.T) {
22+
updateCmd := etcdUpdateCmd()
23+
updateCmd.SetArgs([]string{})
24+
err := updateCmd.Execute()
25+
assert.ErrorContains(t, err, "requires at least 1 arg(s)")
26+
})
27+
28+
t.Run("rejects_invalid_peer_address_flag", func(t *testing.T) {
29+
updateCmd := etcdUpdateCmd()
30+
updateCmd.SetArgs([]string{"--peer-address=neither/ip/nor/name", "peer1"})
31+
err := updateCmd.Execute()
32+
assert.ErrorContains(t, err, `invalid argument "neither/ip/nor/name" for "--peer-address" flag: neither an IP address nor a DNS name`)
33+
})
34+
35+
t.Run("rejects_invalid_member_name_flag", func(t *testing.T) {
36+
updateCmd := etcdUpdateCmd()
37+
updateCmd.SetArgs([]string{"--member-name=neither/ip/nor/name", "peer1"})
38+
err := updateCmd.Execute()
39+
assert.ErrorContains(t, err, `invalid argument "neither/ip/nor/name" for "--member-name" flag: neither an IP address nor a DNS name`)
40+
})
41+
42+
t.Run("usage_string_contains_flag_help", func(t *testing.T) {
43+
updateCmd := etcdUpdateCmd()
44+
usageLines := strings.Split(updateCmd.UsageString(), "\n")
45+
assert.Contains(t, usageLines, " --peer-address ip-or-dns-name etcd peer address to update (default <this node's peer address>)")
46+
assert.Contains(t, usageLines, " --member-name ip-or-dns-name etcd member name to update")
47+
})
48+
}

cmd/root_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ func TestUnknownSubCommandsAreRejected(t *testing.T) {
105105
for _, cmd := range underTest.Commands() {
106106
name, _, _ := strings.Cut(cmd.Use, " ")
107107
require.NotEmpty(t, name)
108-
t.Run(name, testCommand(cmd, slices.Concat(args, []string{name})))
108+
switch name {
109+
case "member-update": // Don't test comands with positional args
110+
default:
111+
t.Run(name, testCommand(cmd, slices.Concat(args, []string{name})))
112+
}
109113
}
110114

111115
subCommand := strings.Join(args, " ")

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)