diff --git a/.go-version b/.go-version index 0e0c284d88ab..ba7b1297a554 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.25.9 +1.25.10 diff --git a/api/go.mod b/api/go.mod index 308d07c5629a..3e78225856b4 100644 --- a/api/go.mod +++ b/api/go.mod @@ -2,7 +2,7 @@ module go.etcd.io/etcd/api/v3 go 1.25.0 -toolchain go1.25.9 +toolchain go1.25.10 require ( github.com/coreos/go-semver v0.3.1 diff --git a/api/version/version.go b/api/version/version.go index 5d748b563c58..b5382afb0b96 100644 --- a/api/version/version.go +++ b/api/version/version.go @@ -26,7 +26,7 @@ import ( var ( // MinClusterVersion is the min cluster version this etcd binary is compatible with. MinClusterVersion = "3.0.0" - Version = "3.6.11" + Version = "3.6.12" APIVersion = "unknown" // Git SHA Value will be set during build diff --git a/client/pkg/fileutil/fileutil_test.go b/client/pkg/fileutil/fileutil_test.go index 6abba29ca593..1c765e4e03c9 100644 --- a/client/pkg/fileutil/fileutil_test.go +++ b/client/pkg/fileutil/fileutil_test.go @@ -19,7 +19,6 @@ import ( "io" "math/rand" "os" - "os/user" "path/filepath" "runtime" "strings" @@ -35,13 +34,7 @@ func TestIsDirWriteable(t *testing.T) { tmpdir := t.TempDir() require.NoErrorf(t, IsDirWriteable(tmpdir), "unexpected IsDirWriteable error") require.NoErrorf(t, os.Chmod(tmpdir, 0o444), "unexpected os.Chmod error") - me, err := user.Current() - if err != nil { - // err can be non-nil when cross compiled - // http://stackoverflow.com/questions/20609415/cross-compiling-user-current-not-implemented-on-linux-amd64 - t.Skipf("failed to get current user: %v", err) - } - if me.Name == "root" || runtime.GOOS == "windows" { + if os.Getuid() == 0 || runtime.GOOS == "windows" { // ideally we should check CAP_DAC_OVERRIDE. // but it does not matter for tests. // Chmod is not supported under windows. diff --git a/client/pkg/go.mod b/client/pkg/go.mod index 0e1be19a12e0..aee6536904df 100644 --- a/client/pkg/go.mod +++ b/client/pkg/go.mod @@ -2,7 +2,7 @@ module go.etcd.io/etcd/client/pkg/v3 go 1.25.0 -toolchain go1.25.9 +toolchain go1.25.10 require ( github.com/coreos/go-systemd/v22 v22.5.0 diff --git a/client/v3/go.mod b/client/v3/go.mod index f7b201b16fd7..d178bbe30556 100644 --- a/client/v3/go.mod +++ b/client/v3/go.mod @@ -2,7 +2,7 @@ module go.etcd.io/etcd/client/v3 go 1.25.0 -toolchain go1.25.9 +toolchain go1.25.10 require ( github.com/coreos/go-semver v0.3.1 @@ -10,8 +10,8 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 github.com/prometheus/client_golang v1.20.5 github.com/stretchr/testify v1.11.1 - go.etcd.io/etcd/api/v3 v3.6.11 - go.etcd.io/etcd/client/pkg/v3 v3.6.11 + go.etcd.io/etcd/api/v3 v3.6.12 + go.etcd.io/etcd/client/pkg/v3 v3.6.12 go.uber.org/zap v1.27.0 google.golang.org/grpc v1.79.3 sigs.k8s.io/yaml v1.4.0 diff --git a/etcdctl/go.mod b/etcdctl/go.mod index dd9278007945..40f4ab793bed 100644 --- a/etcdctl/go.mod +++ b/etcdctl/go.mod @@ -2,7 +2,7 @@ module go.etcd.io/etcd/etcdctl/v3 go 1.25.0 -toolchain go1.25.9 +toolchain go1.25.10 require ( github.com/bgentry/speakeasy v0.2.0 @@ -12,10 +12,10 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - go.etcd.io/etcd/api/v3 v3.6.11 - go.etcd.io/etcd/client/pkg/v3 v3.6.11 - go.etcd.io/etcd/client/v3 v3.6.11 - go.etcd.io/etcd/pkg/v3 v3.6.11 + go.etcd.io/etcd/api/v3 v3.6.12 + go.etcd.io/etcd/client/pkg/v3 v3.6.12 + go.etcd.io/etcd/client/v3 v3.6.12 + go.etcd.io/etcd/pkg/v3 v3.6.12 go.uber.org/zap v1.27.0 golang.org/x/time v0.9.0 google.golang.org/grpc v1.79.3 diff --git a/etcdutl/etcdutl/common.go b/etcdutl/etcdutl/common.go index c7473cc7fd7b..ebdbade2c7e3 100644 --- a/etcdutl/etcdutl/common.go +++ b/etcdutl/etcdutl/common.go @@ -16,6 +16,7 @@ package etcdutl import ( "errors" + "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -29,6 +30,19 @@ import ( "go.etcd.io/raft/v3/raftpb" ) +// validateDataDir checks if the data directory's backend database file exists. +func validateDataDir(dataDir string) error { + dbPath := datadir.ToBackendFileName(dataDir) + return validateFilePath(dbPath) +} + +// validateFilePath checks if the specified file exists. +// It returns an error encountered by os.Stat, such as os.ErrNotExist, os.ErrPermission, etc. +func validateFilePath(filePath string) error { + _, err := os.Stat(filePath) + return err +} + func GetLogger() *zap.Logger { config := logutil.DefaultZapLoggerConfig config.Encoding = "console" diff --git a/etcdutl/etcdutl/defrag_command.go b/etcdutl/etcdutl/defrag_command.go index 5135f5f5eb85..b57572905a91 100644 --- a/etcdutl/etcdutl/defrag_command.go +++ b/etcdutl/etcdutl/defrag_command.go @@ -50,8 +50,13 @@ func defragCommandFunc(cmd *cobra.Command, args []string) { } func DefragData(dataDir string) error { - var be backend.Backend - lg := GetLogger() + var ( + be backend.Backend + lg = GetLogger() + ) + if err := validateDataDir(dataDir); err != nil { + return err + } bch := make(chan struct{}) dbDir := datadir.ToBackendFileName(dataDir) go func() { diff --git a/etcdutl/etcdutl/hashkv_command.go b/etcdutl/etcdutl/hashkv_command.go index 20c47a514521..5a5a740dd283 100644 --- a/etcdutl/etcdutl/hashkv_command.go +++ b/etcdutl/etcdutl/hashkv_command.go @@ -54,6 +54,9 @@ type HashKV struct { } func calculateHashKV(dbPath string, rev int64) (HashKV, error) { + if err := validateFilePath(dbPath); err != nil { + return HashKV{}, err + } cfg := backend.DefaultBackendConfig(zap.NewNop()) cfg.Path = dbPath b := backend.New(cfg) diff --git a/etcdutl/etcdutl/migrate_command.go b/etcdutl/etcdutl/migrate_command.go index a7f3d849f168..bfe0c058ac66 100644 --- a/etcdutl/etcdutl/migrate_command.go +++ b/etcdutl/etcdutl/migrate_command.go @@ -122,6 +122,9 @@ func (c *migrateConfig) finalize() error { } func migrateCommandFunc(c *migrateConfig) error { + if err := validateDataDir(c.dataDir); err != nil { + return err + } dbPath := datadir.ToBackendFileName(c.dataDir) be := backend.NewDefaultBackend(GetLogger(), dbPath) defer be.Close() diff --git a/etcdutl/etcdutl/snapshot_command.go b/etcdutl/etcdutl/snapshot_command.go index 52cb4a1bbdda..e0d4b07329e5 100644 --- a/etcdutl/etcdutl/snapshot_command.go +++ b/etcdutl/etcdutl/snapshot_command.go @@ -94,6 +94,9 @@ func SnapshotStatusCommandFunc(cmd *cobra.Command, args []string) { err := fmt.Errorf("snapshot status requires exactly one argument") cobrautl.ExitWithError(cobrautl.ExitBadArgs, err) } + if err := validateFilePath(args[0]); err != nil { + cobrautl.ExitWithError(cobrautl.ExitError, err) + } printer := initPrinterFromCmd(cmd) lg := GetLogger() diff --git a/etcdutl/go.mod b/etcdutl/go.mod index df0ad1f0d10f..7131d96b4322 100644 --- a/etcdutl/go.mod +++ b/etcdutl/go.mod @@ -2,7 +2,7 @@ module go.etcd.io/etcd/etcdutl/v3 go 1.25.0 -toolchain go1.25.9 +toolchain go1.25.10 replace ( go.etcd.io/etcd/api/v3 => ../api @@ -27,11 +27,11 @@ require ( github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 go.etcd.io/bbolt v1.4.3 - go.etcd.io/etcd/api/v3 v3.6.11 - go.etcd.io/etcd/client/pkg/v3 v3.6.11 - go.etcd.io/etcd/client/v3 v3.6.11 - go.etcd.io/etcd/pkg/v3 v3.6.11 - go.etcd.io/etcd/server/v3 v3.6.11 + go.etcd.io/etcd/api/v3 v3.6.12 + go.etcd.io/etcd/client/pkg/v3 v3.6.12 + go.etcd.io/etcd/client/v3 v3.6.12 + go.etcd.io/etcd/pkg/v3 v3.6.12 + go.etcd.io/etcd/server/v3 v3.6.12 go.etcd.io/raft/v3 v3.6.0 go.uber.org/zap v1.27.0 ) diff --git a/go.mod b/go.mod index 1c42fccfd331..7e567ca53f95 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module go.etcd.io/etcd/v3 go 1.25.0 -toolchain go1.25.9 +toolchain go1.25.10 replace ( go.etcd.io/etcd/api/v3 => ./api @@ -24,14 +24,14 @@ require ( github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 go.etcd.io/bbolt v1.4.3 - go.etcd.io/etcd/api/v3 v3.6.11 - go.etcd.io/etcd/client/pkg/v3 v3.6.11 - go.etcd.io/etcd/client/v3 v3.6.11 - go.etcd.io/etcd/etcdctl/v3 v3.6.11 - go.etcd.io/etcd/etcdutl/v3 v3.6.11 - go.etcd.io/etcd/pkg/v3 v3.6.11 - go.etcd.io/etcd/server/v3 v3.6.11 - go.etcd.io/etcd/tests/v3 v3.6.11 + go.etcd.io/etcd/api/v3 v3.6.12 + go.etcd.io/etcd/client/pkg/v3 v3.6.12 + go.etcd.io/etcd/client/v3 v3.6.12 + go.etcd.io/etcd/etcdctl/v3 v3.6.12 + go.etcd.io/etcd/etcdutl/v3 v3.6.12 + go.etcd.io/etcd/pkg/v3 v3.6.12 + go.etcd.io/etcd/server/v3 v3.6.12 + go.etcd.io/etcd/tests/v3 v3.6.12 go.etcd.io/raft/v3 v3.6.0 go.uber.org/zap v1.27.0 golang.org/x/time v0.9.0 diff --git a/pkg/go.mod b/pkg/go.mod index 7eba5f49b0ca..e0e2841ac4f4 100644 --- a/pkg/go.mod +++ b/pkg/go.mod @@ -2,7 +2,7 @@ module go.etcd.io/etcd/pkg/v3 go 1.25.0 -toolchain go1.25.9 +toolchain go1.25.10 require ( github.com/creack/pty v1.1.18 @@ -10,7 +10,7 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - go.etcd.io/etcd/client/pkg/v3 v3.6.11 + go.etcd.io/etcd/client/pkg/v3 v3.6.12 go.uber.org/zap v1.27.0 google.golang.org/grpc v1.79.3 ) diff --git a/server/etcdserver/api/membership/cluster.go b/server/etcdserver/api/membership/cluster.go index 299e61374cea..3378c96992b8 100644 --- a/server/etcdserver/api/membership/cluster.go +++ b/server/etcdserver/api/membership/cluster.go @@ -619,24 +619,31 @@ func (c *RaftCluster) UpdateRaftAttributes(id types.ID, raftAttr RaftAttributes, c.Lock() defer c.Unlock() + var ( + m *Member + ok bool + ) + + if m, ok = c.members[id]; !ok { + c.lg.Info("Skipped updating non-existent member in v2store", + zap.String("cluster-id", c.cid.String()), + zap.String("local-member-id", c.localID.String()), + zap.String("updated-remote-peer-id", id.String()), + zap.Strings("updated-remote-peer-urls", raftAttr.PeerURLs), + zap.Bool("updated-remote-peer-is-learner", raftAttr.IsLearner), + zap.Bool("should-apply-v3", bool(shouldApplyV3)), + ) + return + } + // `MemberUpdateRequest` only supports updating PeerURLs. + m.RaftAttributes.PeerURLs = raftAttr.PeerURLs + if c.v2store != nil { - if _, ok := c.members[id]; ok { - m := *(c.members[id]) - m.RaftAttributes = raftAttr - mustUpdateMemberInStore(c.lg, c.v2store, &m) - } else { - c.lg.Info("Skipped updating non-existent member in v2store", - zap.String("cluster-id", c.cid.String()), - zap.String("local-member-id", c.localID.String()), - zap.String("updated-remote-peer-id", id.String()), - zap.Strings("updated-remote-peer-urls", raftAttr.PeerURLs), - zap.Bool("updated-remote-peer-is-learner", raftAttr.IsLearner), - ) - } + mustUpdateMemberInStore(c.lg, c.v2store, m) } + if c.be != nil && shouldApplyV3 { - c.members[id].RaftAttributes = raftAttr - c.be.MustSaveMemberToBackend(c.members[id]) + c.be.MustSaveMemberToBackend(m) c.lg.Info( "updated member", diff --git a/server/etcdserver/api/membership/cluster_test.go b/server/etcdserver/api/membership/cluster_test.go index 62f0619f2aac..da1f5313e3a6 100644 --- a/server/etcdserver/api/membership/cluster_test.go +++ b/server/etcdserver/api/membership/cluster_test.go @@ -1064,7 +1064,7 @@ func TestUpdateRaftAttributes(t *testing.T) { wantMembers map[types.ID]*Member }{ { - name: "update an existing member", + name: "update an existing voting member", members: []*Member{ newTestMember(1, oldPeerURLs, "1", clientURLs), newTestMember(2, oldPeerURLs, "2", clientURLs), @@ -1075,6 +1075,18 @@ func TestUpdateRaftAttributes(t *testing.T) { 2: newTestMember(2, newPeerURLs, "2", clientURLs), }, }, + { + name: "update an existing learner member", + members: []*Member{ + newTestMember(1, oldPeerURLs, "1", clientURLs), + newTestMemberAsLearner(2, oldPeerURLs, "2", clientURLs), + }, + updateMemberID: 2, + wantMembers: map[types.ID]*Member{ + 1: newTestMember(1, oldPeerURLs, "1", clientURLs), + 2: newTestMemberAsLearner(2, newPeerURLs, "2", clientURLs), + }, + }, { name: "update a non-exist member", members: []*Member{ @@ -1100,6 +1112,7 @@ func TestUpdateRaftAttributes(t *testing.T) { mst, _ := membersFromStore(c.lg, st) require.Equal(t, tc.wantMembers, mst) + require.Equal(t, tc.wantMembers, c.members) }) } } diff --git a/server/etcdserver/api/v3rpc/maintenance.go b/server/etcdserver/api/v3rpc/maintenance.go index cf65dad0da7c..5979c216d8b8 100644 --- a/server/etcdserver/api/v3rpc/maintenance.go +++ b/server/etcdserver/api/v3rpc/maintenance.go @@ -364,7 +364,7 @@ func (ams *authMaintenanceServer) Alarm(ctx context.Context, ar *pb.AlarmRequest } func (ams *authMaintenanceServer) Status(ctx context.Context, ar *pb.StatusRequest) (*pb.StatusResponse, error) { - if err := ams.isPermitted(ctx); err != nil { + if err := ams.requireAuthInfo(ctx); err != nil { return nil, togRPCError(err) } diff --git a/server/go.mod b/server/go.mod index 1967d8ed68cf..69d817d5044f 100644 --- a/server/go.mod +++ b/server/go.mod @@ -2,7 +2,7 @@ module go.etcd.io/etcd/server/v3 go 1.25.0 -toolchain go1.25.9 +toolchain go1.25.10 require ( github.com/coreos/go-semver v0.3.1 @@ -26,10 +26,10 @@ require ( github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 go.etcd.io/bbolt v1.4.3 - go.etcd.io/etcd/api/v3 v3.6.11 - go.etcd.io/etcd/client/pkg/v3 v3.6.11 - go.etcd.io/etcd/client/v3 v3.6.11 - go.etcd.io/etcd/pkg/v3 v3.6.11 + go.etcd.io/etcd/api/v3 v3.6.12 + go.etcd.io/etcd/client/pkg/v3 v3.6.12 + go.etcd.io/etcd/client/v3 v3.6.12 + go.etcd.io/etcd/pkg/v3 v3.6.12 go.etcd.io/raft/v3 v3.6.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 go.opentelemetry.io/otel v1.40.0 diff --git a/tests/common/maintenance_auth_test.go b/tests/common/maintenance_auth_test.go index 21a72580ede4..12f3e8b643ca 100644 --- a/tests/common/maintenance_auth_test.go +++ b/tests/common/maintenance_auth_test.go @@ -166,7 +166,7 @@ func TestStatusWithRootAuth(t *testing.T) { } func TestStatusWithUserAuth(t *testing.T) { - testStatusWithAuth(t, false, true, WithAuth("user0", "user0Pass")) + testStatusWithAuth(t, false, false, WithAuth("user0", "user0Pass")) } func testStatusWithAuth(t *testing.T, expectConnectionError, expectOperationError bool, opts ...config.ClientOption) { diff --git a/tests/go.mod b/tests/go.mod index cf43b1947433..d14bb9c69703 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -2,7 +2,7 @@ module go.etcd.io/etcd/tests/v3 go 1.25.0 -toolchain go1.25.9 +toolchain go1.25.10 replace ( go.etcd.io/etcd/api/v3 => ../api @@ -28,14 +28,14 @@ require ( github.com/soheilhy/cmux v0.1.5 github.com/stretchr/testify v1.11.1 go.etcd.io/bbolt v1.4.3 - go.etcd.io/etcd/api/v3 v3.6.11 - go.etcd.io/etcd/client/pkg/v3 v3.6.11 + go.etcd.io/etcd/api/v3 v3.6.12 + go.etcd.io/etcd/client/pkg/v3 v3.6.12 go.etcd.io/etcd/client/v2 v2.305.20 - go.etcd.io/etcd/client/v3 v3.6.11 - go.etcd.io/etcd/etcdctl/v3 v3.6.11 - go.etcd.io/etcd/etcdutl/v3 v3.6.11 - go.etcd.io/etcd/pkg/v3 v3.6.11 - go.etcd.io/etcd/server/v3 v3.6.11 + go.etcd.io/etcd/client/v3 v3.6.12 + go.etcd.io/etcd/etcdctl/v3 v3.6.12 + go.etcd.io/etcd/etcdutl/v3 v3.6.12 + go.etcd.io/etcd/pkg/v3 v3.6.12 + go.etcd.io/etcd/server/v3 v3.6.12 go.etcd.io/gofail v0.2.0 go.etcd.io/raft/v3 v3.6.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 diff --git a/tests/integration/clientv3/cluster_test.go b/tests/integration/clientv3/cluster_test.go index 055da70f3e7f..a4a9f24c0aa1 100644 --- a/tests/integration/clientv3/cluster_test.go +++ b/tests/integration/clientv3/cluster_test.go @@ -25,7 +25,9 @@ import ( "github.com/stretchr/testify/require" + "go.etcd.io/etcd/api/v3/etcdserverpb" "go.etcd.io/etcd/client/pkg/v3/types" + clientv3 "go.etcd.io/etcd/client/v3" integration2 "go.etcd.io/etcd/tests/v3/framework/integration" ) @@ -156,6 +158,33 @@ func TestMemberUpdate(t *testing.T) { } } +func TestMemberUpdateLearner(t *testing.T) { + integration2.BeforeTest(t) + + clus := integration2.NewCluster(t, &integration2.ClusterConfig{Size: 3, DisableStrictReconfigCheck: true}) + defer clus.Terminate(t) + + capi := clus.RandClient() + + urls := []string{"http://127.0.0.1:1234"} + addResp, err := capi.MemberAddAsLearner(t.Context(), urls) + require.NoError(t, err) + learnerID := addResp.Member.ID + + learner, err := getMemberByID(t.Context(), capi, learnerID) + require.NoError(t, err) + require.Truef(t, learner.IsLearner, "added a member as learner, IsLearner is %t", learner.IsLearner) + + updatedURLs := []string{"http://127.0.0.1:5678"} + _, err = capi.MemberUpdate(t.Context(), learnerID, updatedURLs) + require.NoError(t, err) + + learner, err = getMemberByID(t.Context(), capi, learnerID) + require.NoError(t, err) + require.Equal(t, learner.PeerURLs, updatedURLs) + require.Truef(t, learner.IsLearner, "updated peer address of a learner member, IsLearner is %t", learner.IsLearner) +} + func TestMemberAddUpdateWrongURLs(t *testing.T) { integration2.BeforeTest(t) @@ -429,3 +458,24 @@ func TestMaxLearnerInCluster(t *testing.T) { t.Errorf("failed to add member %v", err) } } + +func getMemberByID(ctx context.Context, cli *clientv3.Client, id uint64) (member *etcdserverpb.Member, err error) { + var resp *clientv3.MemberListResponse + resp, err = cli.MemberList(ctx) + if err != nil { + return member, err + } + + for _, m := range resp.Members { + if m.ID == id { + member = m + break + } + } + + if member == nil { + err = fmt.Errorf("failed to find member by id %d", id) + } + + return member, err +} diff --git a/tools/mod/go.mod b/tools/mod/go.mod index ce779590de04..6ce1bd68e87a 100644 --- a/tools/mod/go.mod +++ b/tools/mod/go.mod @@ -2,7 +2,7 @@ module go.etcd.io/etcd/tools/v3 go 1.25.0 -toolchain go1.25.9 +toolchain go1.25.10 require ( github.com/alexfalkowski/gocovmerge v1.3.18 diff --git a/tools/rw-heatmaps/go.mod b/tools/rw-heatmaps/go.mod index 964f5c840d47..f6c63942eac1 100644 --- a/tools/rw-heatmaps/go.mod +++ b/tools/rw-heatmaps/go.mod @@ -2,7 +2,7 @@ module go.etcd.io/etcd/tools/rw-heatmaps/v3 go 1.25.0 -toolchain go1.25.9 +toolchain go1.25.10 require ( github.com/spf13/cobra v1.10.2 diff --git a/tools/testgrid-analysis/go.mod b/tools/testgrid-analysis/go.mod index bdb938603918..742ca295520b 100644 --- a/tools/testgrid-analysis/go.mod +++ b/tools/testgrid-analysis/go.mod @@ -2,7 +2,7 @@ module go.etcd.io/etcd/tools/testgrid-analysis/v3 go 1.25.0 -toolchain go1.25.9 +toolchain go1.25.10 require ( github.com/GoogleCloudPlatform/testgrid v0.0.173