Skip to content

Commit 9971b60

Browse files
authored
Merge pull request #4118 from twz123/harden-etcd-cli
Harden etcd subcommand usage and validation
2 parents 8530d05 + 477d354 commit 9971b60

File tree

4 files changed

+119
-12
lines changed

4 files changed

+119
-12
lines changed

cmd/etcd/leave.go

+39-11
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,27 @@ limitations under the License.
1717
package etcd
1818

1919
import (
20+
"errors"
2021
"fmt"
22+
"net"
23+
"net/url"
2124

2225
"github.com/k0sproject/k0s/pkg/config"
2326
"github.com/k0sproject/k0s/pkg/etcd"
2427

28+
"github.com/asaskevich/govalidator"
2529
"github.com/sirupsen/logrus"
2630
"github.com/spf13/cobra"
31+
"github.com/spf13/pflag"
2732
)
2833

2934
func etcdLeaveCmd() *cobra.Command {
30-
var etcdPeerAddress string
35+
var peerAddressArg string
3136

3237
cmd := &cobra.Command{
3338
Use: "leave",
34-
Short: "Sign off a given etc node from etcd cluster",
39+
Short: "Leave the etcd cluster, or remove a specific peer",
40+
Args: cobra.NoArgs, // accept peer address via flag, not via arg
3541
RunE: func(cmd *cobra.Command, args []string) error {
3642
opts, err := config.GetCmdOpts(cmd)
3743
if err != nil {
@@ -42,14 +48,17 @@ func etcdLeaveCmd() *cobra.Command {
4248
return err
4349
}
4450
ctx := cmd.Context()
45-
if etcdPeerAddress == "" {
46-
etcdPeerAddress = nodeConfig.Spec.Storage.Etcd.PeerAddress
47-
}
48-
if etcdPeerAddress == "" {
49-
return fmt.Errorf("can't leave etcd cluster: peer address is empty, check the config file or use cli argument")
51+
52+
peerAddress := nodeConfig.Spec.Storage.Etcd.PeerAddress
53+
if peerAddressArg == "" {
54+
if peerAddress == "" {
55+
return fmt.Errorf("can't leave etcd cluster: this node doesn't have an etcd peer address, check the k0s configuration or use --peer-address")
56+
}
57+
} else {
58+
peerAddress = peerAddressArg
5059
}
5160

52-
peerURL := fmt.Sprintf("https://%s:2380", etcdPeerAddress)
61+
peerURL := (&url.URL{Scheme: "https", Host: net.JoinHostPort(peerAddress, "2380")}).String()
5362
etcdClient, err := etcd.NewClient(opts.K0sVars.CertRootDir, opts.K0sVars.EtcdCertDir, nodeConfig.Spec.Storage.Etcd)
5463
if err != nil {
5564
return fmt.Errorf("can't connect to the etcd: %w", err)
@@ -64,19 +73,38 @@ func etcdLeaveCmd() *cobra.Command {
6473
if err := etcdClient.DeleteMember(ctx, peerID); err != nil {
6574
logrus.
6675
WithField("peerURL", peerURL).
67-
WithField("peerID", peerID).
76+
WithField("peerID", fmt.Sprintf("%x", peerID)).
6877
Errorf("Failed to delete node from cluster")
6978
return err
7079
}
7180

7281
logrus.
73-
WithField("peerID", peerID).
82+
WithField("peerID", fmt.Sprintf("%x", peerID)).
7483
Info("Successfully deleted")
7584
return nil
7685
},
7786
}
7887

79-
cmd.Flags().StringVar(&etcdPeerAddress, "peer-address", "", "etcd peer address")
88+
cmd.Flags().AddFlag(&pflag.Flag{
89+
Name: "peer-address",
90+
Usage: "etcd peer address to remove (default <this node's peer address>)",
91+
Value: (*ipOrDNSName)(&peerAddressArg),
92+
})
93+
8094
cmd.PersistentFlags().AddFlagSet(config.GetPersistentFlagSet())
8195
return cmd
8296
}
97+
98+
type ipOrDNSName string
99+
100+
func (i *ipOrDNSName) Type() string { return "ip-or-dns-name" }
101+
func (i *ipOrDNSName) String() string { return string(*i) }
102+
103+
func (i *ipOrDNSName) Set(value string) error {
104+
if !govalidator.IsIP(value) && !govalidator.IsDNSName(value) {
105+
return errors.New("neither an IP address nor a DNS name")
106+
}
107+
108+
*i = ipOrDNSName(value)
109+
return nil
110+
}

cmd/etcd/leave_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
Copyright 2024 k0s authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package etcd
18+
19+
import (
20+
"strings"
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
)
25+
26+
func TestEtcdLeaveCmd(t *testing.T) {
27+
t.Run("rejects_IP_address_as_arg", func(t *testing.T) {
28+
leaveCmd := etcdLeaveCmd()
29+
leaveCmd.SetArgs([]string{"255.255.255.255"})
30+
err := leaveCmd.Execute()
31+
assert.ErrorContains(t, err, `unknown command "255.255.255.255" for "leave"`)
32+
})
33+
34+
t.Run("rejects_invalid_peer_addresses", func(t *testing.T) {
35+
leaveCmd := etcdLeaveCmd()
36+
leaveCmd.SetArgs([]string{"--peer-address=neither/ip/nor/name"})
37+
err := leaveCmd.Execute()
38+
assert.ErrorContains(t, err, `invalid argument "neither/ip/nor/name" for "--peer-address" flag: neither an IP address nor a DNS name`)
39+
})
40+
41+
t.Run("peer_address_usage_string", func(t *testing.T) {
42+
leaveCmd := etcdLeaveCmd()
43+
usageLines := strings.Split(leaveCmd.UsageString(), "\n")
44+
assert.Contains(t, usageLines, " --peer-address ip-or-dns-name etcd peer address to remove (default <this node's peer address>)")
45+
})
46+
}

cmd/etcd/list.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import (
3030
func etcdListCmd() *cobra.Command {
3131
cmd := &cobra.Command{
3232
Use: "member-list",
33-
Short: "Returns etcd cluster members list",
33+
Short: "List etcd cluster members (JSON encoded)",
34+
Args: cobra.NoArgs,
3435
PreRun: func(cmd *cobra.Command, args []string) {
3536
// ensure logs don't mess up the output
3637
logrus.SetOutput(cmd.ErrOrStderr())

cmd/etcd/list_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
Copyright 2024 k0s authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package etcd
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
)
24+
25+
func TestEtcdListCmd(t *testing.T) {
26+
t.Run("rejects_args", func(t *testing.T) {
27+
leaveCmd := etcdListCmd()
28+
leaveCmd.SetArgs([]string{"bogus"})
29+
err := leaveCmd.Execute()
30+
assert.ErrorContains(t, err, `unknown command "bogus" for "member-list"`)
31+
})
32+
}

0 commit comments

Comments
 (0)