k8s.io/kubernetes@v1.29.3/pkg/volume/util/fsquota/project.go (about) 1 //go:build linux 2 // +build linux 3 4 /* 5 Copyright 2018 The Kubernetes Authors. 6 7 Licensed under the Apache License, Version 2.0 (the "License"); 8 you may not use this file except in compliance with the License. 9 You may obtain a copy of the License at 10 11 http://www.apache.org/licenses/LICENSE-2.0 12 13 Unless required by applicable law or agreed to in writing, software 14 distributed under the License is distributed on an "AS IS" BASIS, 15 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 See the License for the specific language governing permissions and 17 limitations under the License. 18 */ 19 20 package fsquota 21 22 import ( 23 "bufio" 24 "fmt" 25 "os" 26 "path/filepath" 27 "regexp" 28 "strconv" 29 "sync" 30 31 "golang.org/x/sys/unix" 32 "k8s.io/kubernetes/pkg/volume/util/fsquota/common" 33 ) 34 35 var projectsFile = "/etc/projects" 36 var projidFile = "/etc/projid" 37 38 var projectsParseRegexp = regexp.MustCompilePOSIX("^([[:digit:]]+):(.*)$") 39 var projidParseRegexp = regexp.MustCompilePOSIX("^([^#][^:]*):([[:digit:]]+)$") 40 41 var quotaIDLock sync.RWMutex 42 43 const maxUnusedQuotasToSearch = 128 // Don't go into an infinite loop searching for an unused quota 44 45 type projectType struct { 46 isValid bool // False if we need to remove this line 47 id common.QuotaID 48 data string // Project name (projid) or directory (projects) 49 line string 50 } 51 52 type projectsList struct { 53 projects []projectType 54 projid []projectType 55 } 56 57 func projFilesAreOK() error { 58 if sf, err := os.Lstat(projectsFile); err != nil || sf.Mode().IsRegular() { 59 if sf, err := os.Lstat(projidFile); err != nil || sf.Mode().IsRegular() { 60 return nil 61 } 62 return fmt.Errorf("%s exists but is not a plain file, cannot continue", projidFile) 63 } 64 return fmt.Errorf("%s exists but is not a plain file, cannot continue", projectsFile) 65 } 66 67 func lockFile(file *os.File) error { 68 return unix.Flock(int(file.Fd()), unix.LOCK_EX) 69 } 70 71 func unlockFile(file *os.File) error { 72 return unix.Flock(int(file.Fd()), unix.LOCK_UN) 73 } 74 75 // openAndLockProjectFiles opens /etc/projects and /etc/projid locked. 76 // Creates them if they don't exist 77 func openAndLockProjectFiles() (*os.File, *os.File, error) { 78 // Make sure neither project-related file is a symlink! 79 if err := projFilesAreOK(); err != nil { 80 return nil, nil, fmt.Errorf("system project files failed verification: %v", err) 81 } 82 // We don't actually modify the original files; we create temporaries and 83 // move them over the originals 84 fProjects, err := os.OpenFile(projectsFile, os.O_RDONLY|os.O_CREATE, 0644) 85 if err != nil { 86 err = fmt.Errorf("unable to open %s: %v", projectsFile, err) 87 return nil, nil, err 88 } 89 fProjid, err := os.OpenFile(projidFile, os.O_RDONLY|os.O_CREATE, 0644) 90 if err == nil { 91 // Check once more, to ensure nothing got changed out from under us 92 if err = projFilesAreOK(); err == nil { 93 err = lockFile(fProjects) 94 if err == nil { 95 err = lockFile(fProjid) 96 if err == nil { 97 return fProjects, fProjid, nil 98 } 99 // Nothing useful we can do if we get an error here 100 err = fmt.Errorf("unable to lock %s: %v", projidFile, err) 101 unlockFile(fProjects) 102 } else { 103 err = fmt.Errorf("unable to lock %s: %v", projectsFile, err) 104 } 105 } else { 106 err = fmt.Errorf("system project files failed re-verification: %v", err) 107 } 108 fProjid.Close() 109 } else { 110 err = fmt.Errorf("unable to open %s: %v", projidFile, err) 111 } 112 fProjects.Close() 113 return nil, nil, err 114 } 115 116 func closeProjectFiles(fProjects *os.File, fProjid *os.File) error { 117 // Nothing useful we can do if either of these fail, 118 // but we have to close (and thereby unlock) the files anyway. 119 var err error 120 var err1 error 121 if fProjid != nil { 122 err = fProjid.Close() 123 } 124 if fProjects != nil { 125 err1 = fProjects.Close() 126 } 127 if err == nil { 128 return err1 129 } 130 return err 131 } 132 133 func parseProject(l string) projectType { 134 if match := projectsParseRegexp.FindStringSubmatch(l); match != nil { 135 i, err := strconv.Atoi(match[1]) 136 if err == nil { 137 return projectType{true, common.QuotaID(i), match[2], l} 138 } 139 } 140 return projectType{true, common.BadQuotaID, "", l} 141 } 142 143 func parseProjid(l string) projectType { 144 if match := projidParseRegexp.FindStringSubmatch(l); match != nil { 145 i, err := strconv.Atoi(match[2]) 146 if err == nil { 147 return projectType{true, common.QuotaID(i), match[1], l} 148 } 149 } 150 return projectType{true, common.BadQuotaID, "", l} 151 } 152 153 func parseProjFile(f *os.File, parser func(l string) projectType) []projectType { 154 var answer []projectType 155 scanner := bufio.NewScanner(f) 156 for scanner.Scan() { 157 answer = append(answer, parser(scanner.Text())) 158 } 159 return answer 160 } 161 162 func readProjectFiles(projects *os.File, projid *os.File) projectsList { 163 return projectsList{parseProjFile(projects, parseProject), parseProjFile(projid, parseProjid)} 164 } 165 166 // findAvailableQuota finds the next available quota from the FirstQuota 167 // it returns error if QuotaIDIsInUse returns error when getting quota id in use; 168 // it searches at most maxUnusedQuotasToSearch(128) time 169 func findAvailableQuota(path string, idMap map[common.QuotaID]bool) (common.QuotaID, error) { 170 unusedQuotasSearched := 0 171 for id := common.FirstQuota; true; id++ { 172 if _, ok := idMap[id]; !ok { 173 isInUse, err := getApplier(path).QuotaIDIsInUse(id) 174 if err != nil { 175 return common.BadQuotaID, err 176 } else if !isInUse { 177 return id, nil 178 } 179 unusedQuotasSearched++ 180 if unusedQuotasSearched > maxUnusedQuotasToSearch { 181 break 182 } 183 } 184 } 185 return common.BadQuotaID, fmt.Errorf("cannot find available quota ID") 186 } 187 188 func addDirToProject(path string, id common.QuotaID, list *projectsList) (common.QuotaID, bool, error) { 189 idMap := make(map[common.QuotaID]bool) 190 for _, project := range list.projects { 191 if project.data == path { 192 if id != common.BadQuotaID && id != project.id { 193 return common.BadQuotaID, false, fmt.Errorf("attempt to reassign project ID for %s", path) 194 } 195 // Trying to reassign a directory to the project it's 196 // already in. Maybe this should be an error, but for 197 // now treat it as an idempotent operation 198 return project.id, false, nil 199 } 200 idMap[project.id] = true 201 } 202 var needToAddProjid = true 203 for _, projid := range list.projid { 204 idMap[projid.id] = true 205 if projid.id == id && id != common.BadQuotaID { 206 needToAddProjid = false 207 } 208 } 209 var err error 210 if id == common.BadQuotaID { 211 id, err = findAvailableQuota(path, idMap) 212 if err != nil { 213 return common.BadQuotaID, false, err 214 } 215 needToAddProjid = true 216 } 217 if needToAddProjid { 218 name := fmt.Sprintf("volume%v", id) 219 line := fmt.Sprintf("%s:%v", name, id) 220 list.projid = append(list.projid, projectType{true, id, name, line}) 221 } 222 line := fmt.Sprintf("%v:%s", id, path) 223 list.projects = append(list.projects, projectType{true, id, path, line}) 224 return id, needToAddProjid, nil 225 } 226 227 func removeDirFromProject(path string, id common.QuotaID, list *projectsList) (bool, error) { 228 if id == common.BadQuotaID { 229 return false, fmt.Errorf("attempt to remove invalid quota ID from %s", path) 230 } 231 foundAt := -1 232 countByID := make(map[common.QuotaID]int) 233 for i, project := range list.projects { 234 if project.data == path { 235 if id != project.id { 236 return false, fmt.Errorf("attempting to remove quota ID %v from path %s, but expecting ID %v", id, path, project.id) 237 } else if foundAt != -1 { 238 return false, fmt.Errorf("found multiple quota IDs for path %s", path) 239 } 240 // Faster and easier than deleting an element 241 list.projects[i].isValid = false 242 foundAt = i 243 } 244 countByID[project.id]++ 245 } 246 if foundAt == -1 { 247 return false, fmt.Errorf("cannot find quota associated with path %s", path) 248 } 249 if countByID[id] <= 1 { 250 // Removing the last entry means that we're no longer using 251 // the quota ID, so remove that as well 252 for i, projid := range list.projid { 253 if projid.id == id { 254 list.projid[i].isValid = false 255 } 256 } 257 return true, nil 258 } 259 return false, nil 260 } 261 262 func writeProjectFile(base *os.File, projects []projectType) (string, error) { 263 oname := base.Name() 264 stat, err := base.Stat() 265 if err != nil { 266 return "", err 267 } 268 mode := stat.Mode() & os.ModePerm 269 f, err := os.CreateTemp(filepath.Dir(oname), filepath.Base(oname)) 270 if err != nil { 271 return "", err 272 } 273 filename := f.Name() 274 if err := os.Chmod(filename, mode); err != nil { 275 return "", err 276 } 277 for _, proj := range projects { 278 if proj.isValid { 279 if _, err := f.WriteString(fmt.Sprintf("%s\n", proj.line)); err != nil { 280 f.Close() 281 os.Remove(filename) 282 return "", err 283 } 284 } 285 } 286 if err := f.Close(); err != nil { 287 os.Remove(filename) 288 return "", err 289 } 290 return filename, nil 291 } 292 293 func writeProjectFiles(fProjects *os.File, fProjid *os.File, writeProjid bool, list projectsList) error { 294 tmpProjects, err := writeProjectFile(fProjects, list.projects) 295 if err == nil { 296 // Ensure that both files are written before we try to rename either. 297 if writeProjid { 298 tmpProjid, err := writeProjectFile(fProjid, list.projid) 299 if err == nil { 300 err = os.Rename(tmpProjid, fProjid.Name()) 301 if err != nil { 302 os.Remove(tmpProjid) 303 } 304 } 305 } 306 if err == nil { 307 err = os.Rename(tmpProjects, fProjects.Name()) 308 if err == nil { 309 return nil 310 } 311 // We're in a bit of trouble here; at this 312 // point we've successfully renamed tmpProjid 313 // to the real thing, but renaming tmpProject 314 // to the real file failed. There's not much we 315 // can do in this position. Anything we could do 316 // to try to undo it would itself be likely to fail. 317 } 318 os.Remove(tmpProjects) 319 } 320 return fmt.Errorf("unable to write project files: %v", err) 321 } 322 323 // if ID is common.BadQuotaID, generate new project id if the dir is not in a project 324 func createProjectID(path string, ID common.QuotaID) (common.QuotaID, error) { 325 quotaIDLock.Lock() 326 defer quotaIDLock.Unlock() 327 fProjects, fProjid, err := openAndLockProjectFiles() 328 if err == nil { 329 defer closeProjectFiles(fProjects, fProjid) 330 list := readProjectFiles(fProjects, fProjid) 331 var writeProjid bool 332 ID, writeProjid, err = addDirToProject(path, ID, &list) 333 if err == nil && ID != common.BadQuotaID { 334 if err = writeProjectFiles(fProjects, fProjid, writeProjid, list); err == nil { 335 return ID, nil 336 } 337 } 338 } 339 return common.BadQuotaID, fmt.Errorf("createProjectID %s %v failed %v", path, ID, err) 340 } 341 342 func removeProjectID(path string, ID common.QuotaID) error { 343 if ID == common.BadQuotaID { 344 return fmt.Errorf("attempting to remove invalid quota ID %v", ID) 345 } 346 quotaIDLock.Lock() 347 defer quotaIDLock.Unlock() 348 fProjects, fProjid, err := openAndLockProjectFiles() 349 if err == nil { 350 defer closeProjectFiles(fProjects, fProjid) 351 list := readProjectFiles(fProjects, fProjid) 352 var writeProjid bool 353 writeProjid, err = removeDirFromProject(path, ID, &list) 354 if err == nil { 355 if err = writeProjectFiles(fProjects, fProjid, writeProjid, list); err == nil { 356 return nil 357 } 358 } 359 } 360 return fmt.Errorf("removeProjectID %s %v failed %v", path, ID, err) 361 }