// Copyright 2021 The etcd Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package schema import ( "fmt" "github.com/coreos/go-semver/semver" "go.etcd.io/etcd/api/v3/version" "go.uber.org/zap" "go.etcd.io/etcd/server/v3/storage/backend" ) var ( V3_5 = semver.Version{Major: 3, Minor: 5} V3_6 = semver.Version{Major: 3, Minor: 6} ) // Validate checks provided backend to confirm that schema used is supported. func Validate(lg *zap.Logger, tx backend.BatchTx) error { tx.Lock() defer tx.Unlock() return unsafeValidate(lg, tx) } func unsafeValidate(lg *zap.Logger, tx backend.BatchTx) error { current, err := UnsafeDetectSchemaVersion(lg, tx) if err != nil { // v3.5 requires a wal snapshot to persist its fields, so we can assign it a schema version. lg.Warn("Failed to detect storage schema version. Please wait till wal snapshot before upgrading cluster.") return nil } _, err = newPlan(lg, current, localBinaryVersion()) return err } func localBinaryVersion() semver.Version { v := semver.New(version.Version) return semver.Version{Major: v.Major, Minor: v.Minor} } // Migrate updates storage schema to provided target version. func Migrate(lg *zap.Logger, tx backend.BatchTx, target semver.Version) error { tx.Lock() defer tx.Unlock() return UnsafeMigrate(lg, tx, target) } // UnsafeMigrate is non-threadsafe version of Migrate. func UnsafeMigrate(lg *zap.Logger, tx backend.BatchTx, target semver.Version) error { current, err := UnsafeDetectSchemaVersion(lg, tx) if err != nil { return fmt.Errorf("cannot detect storage schema version: %w", err) } plan, err := newPlan(lg, current, target) if err != nil { return fmt.Errorf("cannot create migration plan: %w", err) } return plan.unsafeExecute(lg, tx) } // DetectSchemaVersion returns version of storage schema. Returned value depends on etcd version that created the backend. For // * v3.6 and newer will return storage version. // * v3.5 will return it's version if it includes all storage fields added in v3.5 (might require a snapshot). // * v3.4 and older is not supported and will return error. func DetectSchemaVersion(lg *zap.Logger, tx backend.ReadTx) (v semver.Version, err error) { tx.Lock() defer tx.Unlock() return UnsafeDetectSchemaVersion(lg, tx) } // UnsafeDetectSchemaVersion non-threadsafe version of DetectSchemaVersion. func UnsafeDetectSchemaVersion(lg *zap.Logger, tx backend.ReadTx) (v semver.Version, err error) { vp := UnsafeReadStorageVersion(tx) if vp != nil { return *vp, nil } confstate := UnsafeConfStateFromBackend(lg, tx) if confstate == nil { return v, fmt.Errorf("missing confstate information") } _, term := UnsafeReadConsistentIndex(tx) if term == 0 { return v, fmt.Errorf("missing term information") } return V3_5, nil } func schemaChangesForVersion(v semver.Version, isUpgrade bool) ([]schemaChange, error) { // changes should be taken from higher version if isUpgrade { v = semver.Version{Major: v.Major, Minor: v.Minor + 1} } actions, found := schemaChanges[v] if !found { return nil, fmt.Errorf("version %q is not supported", v.String()) } return actions, nil } var ( // schemaChanges list changes that were introduced in perticular version. // schema was introduced in v3.6 as so its changes were not tracked before. schemaChanges = map[semver.Version][]schemaChange{ V3_6: { addNewField(Meta, MetaStorageVersionName, emptyStorageVersion), }, } // emptyStorageVersion is used for v3.6 Step for the first time, in all other version StoragetVersion should be set by migrator. // Adding a addNewField for StorageVersion we can reuselogic to remove it when downgrading to v3.5 emptyStorageVersion = []byte("") )