k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cluster/images/etcd/migrate/data_dir.go (about) 1 /* 2 Copyright 2018 The Kubernetes 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 main 18 19 import ( 20 "fmt" 21 "io" 22 "os" 23 "path/filepath" 24 "strings" 25 26 "k8s.io/klog/v2" 27 ) 28 29 // DataDirectory provides utilities for initializing and backing up an 30 // etcd "data-dir" as well as managing a version.txt file to track the 31 // etcd server version and storage version of the etcd data in the 32 // directory. 33 type DataDirectory struct { 34 path string 35 versionFile *VersionFile 36 } 37 38 // OpenOrCreateDataDirectory opens a data directory, creating the directory 39 // if it doesn't not already exist. 40 func OpenOrCreateDataDirectory(path string) (*DataDirectory, error) { 41 exists, err := exists(path) 42 if err != nil { 43 return nil, err 44 } 45 if !exists { 46 klog.Infof("data directory '%s' does not exist, creating it", path) 47 err := os.MkdirAll(path, 0777) 48 if err != nil { 49 return nil, fmt.Errorf("failed to create data directory %s: %v", path, err) 50 } 51 } 52 versionFile := &VersionFile{ 53 path: filepath.Join(path, versionFilename), 54 } 55 return &DataDirectory{path, versionFile}, nil 56 } 57 58 // Initialize set the version.txt to the target version if the data 59 // directory is empty. If the data directory is non-empty, no 60 // version.txt file will be written since the actual version of etcd 61 // used to create the data is unknown. 62 func (d *DataDirectory) Initialize(target *EtcdVersionPair) error { 63 isEmpty, err := d.IsEmpty() 64 if err != nil { 65 return err 66 } 67 if isEmpty { 68 klog.Infof("data directory '%s' is empty, writing target version '%s' to version.txt", d.path, target) 69 err = d.versionFile.Write(target) 70 if err != nil { 71 return fmt.Errorf("failed to write version.txt to '%s': %v", d.path, err) 72 } 73 return nil 74 } 75 return nil 76 } 77 78 // Backup creates a backup copy of data directory. 79 func (d *DataDirectory) Backup() error { 80 backupDir := fmt.Sprintf("%s.bak", d.path) 81 err := os.RemoveAll(backupDir) 82 if err != nil { 83 return err 84 } 85 err = os.MkdirAll(backupDir, 0777) 86 if err != nil { 87 return err 88 } 89 err = copyDirectory(d.path, backupDir) 90 if err != nil { 91 return err 92 } 93 94 return nil 95 } 96 97 // IsEmpty returns true if the data directory is entirely empty. 98 func (d *DataDirectory) IsEmpty() (bool, error) { 99 dir, err := os.Open(d.path) 100 if err != nil { 101 return false, fmt.Errorf("failed to open data directory %s: %v", d.path, err) 102 } 103 defer dir.Close() 104 _, err = dir.Readdirnames(1) 105 if err == io.EOF { 106 return true, nil 107 } 108 return false, err 109 } 110 111 // String returns the data directory path. 112 func (d *DataDirectory) String() string { 113 return d.path 114 } 115 116 // VersionFile provides utilities for reading and writing version.txt files 117 // to etcd "data-dir" for tracking the etcd server and storage versions 118 // of the data in the directory. 119 type VersionFile struct { 120 path string 121 } 122 123 func (v *VersionFile) nextPath() string { 124 return fmt.Sprintf("%s-next", v.path) 125 } 126 127 // Exists returns true if a version.txt file exists on the file system. 128 func (v *VersionFile) Exists() (bool, error) { 129 return exists(v.path) 130 } 131 132 // Read parses the version.txt file and returns it's contents. 133 func (v *VersionFile) Read() (*EtcdVersionPair, error) { 134 data, err := os.ReadFile(v.path) 135 if err != nil { 136 return nil, fmt.Errorf("failed to read version file %s: %v", v.path, err) 137 } 138 txt := strings.TrimSpace(string(data)) 139 vp, err := ParseEtcdVersionPair(txt) 140 if err != nil { 141 return nil, fmt.Errorf("failed to parse etcd '<version>/<storage-version>' string from version.txt file contents '%s': %v", txt, err) 142 } 143 return vp, nil 144 } 145 146 // equals returns true iff VersionFile exists and contains given EtcdVersionPair. 147 func (v *VersionFile) equals(vp *EtcdVersionPair) (bool, error) { 148 exists, err := v.Exists() 149 if err != nil { 150 return false, err 151 } 152 if !exists { 153 return false, nil 154 } 155 cvp, err := v.Read() 156 if err != nil { 157 return false, err 158 } 159 return vp.Equals(cvp), nil 160 } 161 162 // Write creates or overwrites the contents of the version.txt file with the given EtcdVersionPair. 163 func (v *VersionFile) Write(vp *EtcdVersionPair) error { 164 // We do write only if file content differs from given EtcdVersionPair. 165 isUpToDate, err := v.equals(vp) 166 if err != nil { 167 return fmt.Errorf("failed to to check if version file %s should be changed: %v", v.path, err) 168 } 169 if isUpToDate { 170 return nil 171 } 172 // We do write + rename instead of just write to protect from version.txt 173 // corruption under full disk condition. 174 // See https://github.com/kubernetes/kubernetes/issues/98989. 175 err = os.WriteFile(v.nextPath(), []byte(vp.String()), 0666) 176 if err != nil { 177 return fmt.Errorf("failed to write new version file %s: %v", v.nextPath(), err) 178 } 179 return os.Rename(v.nextPath(), v.path) 180 } 181 182 func exists(path string) (bool, error) { 183 if _, err := os.Stat(path); os.IsNotExist(err) { 184 return false, nil 185 } else if err != nil { 186 return false, err 187 } 188 return true, nil 189 }