mirror of https://github.com/k3s-io/k3s
358 lines
10 KiB
Go
358 lines
10 KiB
Go
// +build linux
|
|
|
|
/*
|
|
Copyright 2018 The Kubernetes 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 quota
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"golang.org/x/sys/unix"
|
|
"k8s.io/kubernetes/pkg/volume/util/quota/common"
|
|
)
|
|
|
|
var projectsFile = "/etc/projects"
|
|
var projidFile = "/etc/projid"
|
|
|
|
var projectsParseRegexp = regexp.MustCompilePOSIX("^([[:digit:]]+):(.*)$")
|
|
var projidParseRegexp = regexp.MustCompilePOSIX("^([^#][^:]*):([[:digit:]]+)$")
|
|
|
|
var quotaIDLock sync.RWMutex
|
|
|
|
const maxUnusedQuotasToSearch = 128 // Don't go into an infinite loop searching for an unused quota
|
|
|
|
type projectType struct {
|
|
isValid bool // False if we need to remove this line
|
|
id common.QuotaID
|
|
data string // Project name (projid) or directory (projects)
|
|
line string
|
|
}
|
|
|
|
type projectsList struct {
|
|
projects []projectType
|
|
projid []projectType
|
|
}
|
|
|
|
func projFilesAreOK() error {
|
|
if sf, err := os.Lstat(projectsFile); err != nil || sf.Mode().IsRegular() {
|
|
if sf, err := os.Lstat(projidFile); err != nil || sf.Mode().IsRegular() {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%s exists but is not a plain file, cannot continue", projidFile)
|
|
}
|
|
return fmt.Errorf("%s exists but is not a plain file, cannot continue", projectsFile)
|
|
}
|
|
|
|
func lockFile(file *os.File) error {
|
|
return unix.Flock(int(file.Fd()), unix.LOCK_EX)
|
|
}
|
|
|
|
func unlockFile(file *os.File) error {
|
|
return unix.Flock(int(file.Fd()), unix.LOCK_UN)
|
|
}
|
|
|
|
// openAndLockProjectFiles opens /etc/projects and /etc/projid locked.
|
|
// Creates them if they don't exist
|
|
func openAndLockProjectFiles() (*os.File, *os.File, error) {
|
|
// Make sure neither project-related file is a symlink!
|
|
if err := projFilesAreOK(); err != nil {
|
|
return nil, nil, fmt.Errorf("system project files failed verification: %v", err)
|
|
}
|
|
// We don't actually modify the original files; we create temporaries and
|
|
// move them over the originals
|
|
fProjects, err := os.OpenFile(projectsFile, os.O_RDONLY|os.O_CREATE, 0644)
|
|
if err != nil {
|
|
err = fmt.Errorf("unable to open %s: %v", projectsFile, err)
|
|
return nil, nil, err
|
|
}
|
|
fProjid, err := os.OpenFile(projidFile, os.O_RDONLY|os.O_CREATE, 0644)
|
|
if err == nil {
|
|
// Check once more, to ensure nothing got changed out from under us
|
|
if err := projFilesAreOK(); err == nil {
|
|
err = lockFile(fProjects)
|
|
if err == nil {
|
|
err = lockFile(fProjid)
|
|
if err == nil {
|
|
return fProjects, fProjid, nil
|
|
}
|
|
// Nothing useful we can do if we get an error here
|
|
err = fmt.Errorf("unable to lock %s: %v", projidFile, err)
|
|
unlockFile(fProjects)
|
|
} else {
|
|
err = fmt.Errorf("unable to lock %s: %v", projectsFile, err)
|
|
}
|
|
} else {
|
|
err = fmt.Errorf("system project files failed re-verification: %v", err)
|
|
}
|
|
fProjid.Close()
|
|
} else {
|
|
err = fmt.Errorf("unable to open %s: %v", projidFile, err)
|
|
}
|
|
fProjects.Close()
|
|
return nil, nil, err
|
|
}
|
|
|
|
func closeProjectFiles(fProjects *os.File, fProjid *os.File) error {
|
|
// Nothing useful we can do if either of these fail,
|
|
// but we have to close (and thereby unlock) the files anyway.
|
|
var err error
|
|
var err1 error
|
|
if fProjid != nil {
|
|
err = fProjid.Close()
|
|
}
|
|
if fProjects != nil {
|
|
err1 = fProjects.Close()
|
|
}
|
|
if err == nil {
|
|
return err1
|
|
}
|
|
return err
|
|
}
|
|
|
|
func parseProject(l string) projectType {
|
|
if match := projectsParseRegexp.FindStringSubmatch(l); match != nil {
|
|
i, err := strconv.Atoi(match[1])
|
|
if err == nil {
|
|
return projectType{true, common.QuotaID(i), match[2], l}
|
|
}
|
|
}
|
|
return projectType{true, common.BadQuotaID, "", l}
|
|
}
|
|
|
|
func parseProjid(l string) projectType {
|
|
if match := projidParseRegexp.FindStringSubmatch(l); match != nil {
|
|
i, err := strconv.Atoi(match[2])
|
|
if err == nil {
|
|
return projectType{true, common.QuotaID(i), match[1], l}
|
|
}
|
|
}
|
|
return projectType{true, common.BadQuotaID, "", l}
|
|
}
|
|
|
|
func parseProjFile(f *os.File, parser func(l string) projectType) []projectType {
|
|
var answer []projectType
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
answer = append(answer, parser(scanner.Text()))
|
|
}
|
|
return answer
|
|
}
|
|
|
|
func readProjectFiles(projects *os.File, projid *os.File) projectsList {
|
|
return projectsList{parseProjFile(projects, parseProject), parseProjFile(projid, parseProjid)}
|
|
}
|
|
|
|
func findAvailableQuota(path string, idMap map[common.QuotaID]bool) (common.QuotaID, error) {
|
|
unusedQuotasSearched := 0
|
|
for id := common.FirstQuota; id == id; id++ {
|
|
if _, ok := idMap[id]; !ok {
|
|
isInUse, err := getApplier(path).QuotaIDIsInUse(id)
|
|
if err != nil {
|
|
return common.BadQuotaID, err
|
|
} else if !isInUse {
|
|
return id, nil
|
|
}
|
|
unusedQuotasSearched++
|
|
if unusedQuotasSearched > maxUnusedQuotasToSearch {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return common.BadQuotaID, fmt.Errorf("Cannot find available quota ID")
|
|
}
|
|
|
|
func addDirToProject(path string, id common.QuotaID, list *projectsList) (common.QuotaID, bool, error) {
|
|
idMap := make(map[common.QuotaID]bool)
|
|
for _, project := range list.projects {
|
|
if project.data == path {
|
|
if id != project.id {
|
|
return common.BadQuotaID, false, fmt.Errorf("Attempt to reassign project ID for %s", path)
|
|
}
|
|
// Trying to reassign a directory to the project it's
|
|
// already in. Maybe this should be an error, but for
|
|
// now treat it as an idempotent operation
|
|
return id, false, nil
|
|
}
|
|
idMap[project.id] = true
|
|
}
|
|
var needToAddProjid = true
|
|
for _, projid := range list.projid {
|
|
idMap[projid.id] = true
|
|
if projid.id == id && id != common.BadQuotaID {
|
|
needToAddProjid = false
|
|
}
|
|
}
|
|
var err error
|
|
if id == common.BadQuotaID {
|
|
id, err = findAvailableQuota(path, idMap)
|
|
if err != nil {
|
|
return common.BadQuotaID, false, err
|
|
}
|
|
needToAddProjid = true
|
|
}
|
|
if needToAddProjid {
|
|
name := fmt.Sprintf("volume%v", id)
|
|
line := fmt.Sprintf("%s:%v", name, id)
|
|
list.projid = append(list.projid, projectType{true, id, name, line})
|
|
}
|
|
line := fmt.Sprintf("%v:%s", id, path)
|
|
list.projects = append(list.projects, projectType{true, id, path, line})
|
|
return id, needToAddProjid, nil
|
|
}
|
|
|
|
func removeDirFromProject(path string, id common.QuotaID, list *projectsList) (bool, error) {
|
|
if id == common.BadQuotaID {
|
|
return false, fmt.Errorf("Attempt to remove invalid quota ID from %s", path)
|
|
}
|
|
foundAt := -1
|
|
countByID := make(map[common.QuotaID]int)
|
|
for i, project := range list.projects {
|
|
if project.data == path {
|
|
if id != project.id {
|
|
return false, fmt.Errorf("Attempting to remove quota ID %v from path %s, but expecting ID %v", id, path, project.id)
|
|
} else if foundAt != -1 {
|
|
return false, fmt.Errorf("Found multiple quota IDs for path %s", path)
|
|
}
|
|
// Faster and easier than deleting an element
|
|
list.projects[i].isValid = false
|
|
foundAt = i
|
|
}
|
|
countByID[project.id]++
|
|
}
|
|
if foundAt == -1 {
|
|
return false, fmt.Errorf("Cannot find quota associated with path %s", path)
|
|
}
|
|
if countByID[id] <= 1 {
|
|
// Removing the last entry means that we're no longer using
|
|
// the quota ID, so remove that as well
|
|
for i, projid := range list.projid {
|
|
if projid.id == id {
|
|
list.projid[i].isValid = false
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func writeProjectFile(base *os.File, projects []projectType) (string, error) {
|
|
oname := base.Name()
|
|
stat, err := base.Stat()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
mode := stat.Mode() & os.ModePerm
|
|
f, err := ioutil.TempFile(filepath.Dir(oname), filepath.Base(oname))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
filename := f.Name()
|
|
if err := os.Chmod(filename, mode); err != nil {
|
|
return "", err
|
|
}
|
|
for _, proj := range projects {
|
|
if proj.isValid {
|
|
if _, err := f.WriteString(fmt.Sprintf("%s\n", proj.line)); err != nil {
|
|
f.Close()
|
|
os.Remove(filename)
|
|
return "", err
|
|
}
|
|
}
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
os.Remove(filename)
|
|
return "", err
|
|
}
|
|
return filename, nil
|
|
}
|
|
|
|
func writeProjectFiles(fProjects *os.File, fProjid *os.File, writeProjid bool, list projectsList) error {
|
|
tmpProjects, err := writeProjectFile(fProjects, list.projects)
|
|
if err == nil {
|
|
// Ensure that both files are written before we try to rename either.
|
|
if writeProjid {
|
|
tmpProjid, err := writeProjectFile(fProjid, list.projid)
|
|
if err == nil {
|
|
err = os.Rename(tmpProjid, fProjid.Name())
|
|
if err != nil {
|
|
os.Remove(tmpProjid)
|
|
}
|
|
}
|
|
}
|
|
if err == nil {
|
|
err = os.Rename(tmpProjects, fProjects.Name())
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
// We're in a bit of trouble here; at this
|
|
// point we've successfully renamed tmpProjid
|
|
// to the real thing, but renaming tmpProject
|
|
// to the real file failed. There's not much we
|
|
// can do in this position. Anything we could do
|
|
// to try to undo it would itself be likely to fail.
|
|
}
|
|
os.Remove(tmpProjects)
|
|
}
|
|
return fmt.Errorf("Unable to write project files: %v", err)
|
|
}
|
|
|
|
func createProjectID(path string, ID common.QuotaID) (common.QuotaID, error) {
|
|
quotaIDLock.Lock()
|
|
defer quotaIDLock.Unlock()
|
|
fProjects, fProjid, err := openAndLockProjectFiles()
|
|
if err == nil {
|
|
defer closeProjectFiles(fProjects, fProjid)
|
|
list := readProjectFiles(fProjects, fProjid)
|
|
writeProjid := true
|
|
ID, writeProjid, err = addDirToProject(path, ID, &list)
|
|
if err == nil && ID != common.BadQuotaID {
|
|
if err = writeProjectFiles(fProjects, fProjid, writeProjid, list); err == nil {
|
|
return ID, nil
|
|
}
|
|
}
|
|
}
|
|
return common.BadQuotaID, fmt.Errorf("createProjectID %s %v failed %v", path, ID, err)
|
|
}
|
|
|
|
func removeProjectID(path string, ID common.QuotaID) error {
|
|
if ID == common.BadQuotaID {
|
|
return fmt.Errorf("attempting to remove invalid quota ID %v", ID)
|
|
}
|
|
quotaIDLock.Lock()
|
|
defer quotaIDLock.Unlock()
|
|
fProjects, fProjid, err := openAndLockProjectFiles()
|
|
if err == nil {
|
|
defer closeProjectFiles(fProjects, fProjid)
|
|
list := readProjectFiles(fProjects, fProjid)
|
|
writeProjid := true
|
|
writeProjid, err = removeDirFromProject(path, ID, &list)
|
|
if err == nil {
|
|
if err = writeProjectFiles(fProjects, fProjid, writeProjid, list); err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return fmt.Errorf("removeProjectID %s %v failed %v", path, ID, err)
|
|
}
|