Dmitry Sharshakov 1a8cc5f8b2
feat: add SELinux labels to volumes
Label mounted filesystems like ephemeral, overlay mounts, as well as data directories (going to become volumes later).

Signed-off-by: Dmitry Sharshakov <dmitry.sharshakov@siderolabs.com>
2024-11-21 14:23:43 +01:00

362 lines
8.3 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package mount handles filesystem mount operations.
package mount
import (
"bufio"
"context"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"time"
"github.com/siderolabs/go-retry/retry"
"golang.org/x/sys/unix"
"github.com/siderolabs/talos/internal/pkg/selinux"
"github.com/siderolabs/talos/pkg/machinery/constants"
)
// Point represents a mount point.
type Point struct {
source string
target string
fstype string
flags uintptr
data string
shared bool
extraDirs []string
selinuxLabel string
}
// NewPointOption is a mount point option.
type NewPointOption func(*Point)
// WithProjectQuota sets the project quota flag.
func WithProjectQuota(enabled bool) NewPointOption {
return func(p *Point) {
if !enabled {
return
}
WithData("prjquota")(p)
}
}
// WithData sets the mount data.
func WithData(data string) NewPointOption {
return func(p *Point) {
if len(p.data) > 0 {
p.data += ","
}
p.data += data
}
}
// WithFlags sets the mount flags.
func WithFlags(flags uintptr) NewPointOption {
return func(p *Point) {
p.flags |= flags
}
}
// WithReadonly sets the read-only flag.
func WithReadonly() NewPointOption {
return WithFlags(unix.MS_RDONLY)
}
// WithShared sets the shared flag.
func WithShared() NewPointOption {
return func(p *Point) {
p.shared = true
}
}
// WithExtraDirs sets the extra directories to be created on mount.
func WithExtraDirs(dirs ...string) NewPointOption {
return func(p *Point) {
p.extraDirs = append(p.extraDirs, dirs...)
}
}
// WithSelinuxLabel sets the mount SELinux label.
func WithSelinuxLabel(label string) NewPointOption {
return func(p *Point) {
p.selinuxLabel = label
}
}
// NewPoint creates a new mount point.
func NewPoint(source, target, fstype string, opts ...NewPointOption) *Point {
p := &Point{
source: source,
target: target,
fstype: fstype,
}
for _, opt := range opts {
opt(p)
}
return p
}
// NewReadonlyOverlay creates a new read-only overlay mount point.
func NewReadonlyOverlay(sources []string, target string, opts ...NewPointOption) *Point {
opts = append(opts, WithReadonly(), WithData("lowerdir="+strings.Join(sources, ":")))
return NewPoint("overlay", target, "overlay", opts...)
}
// NewVarOverlay creates a new /var overlay mount point.
func NewVarOverlay(sources []string, target string, opts ...NewPointOption) *Point {
return NewOverlayWithBasePath(sources, target, constants.VarSystemOverlaysPath, opts...)
}
// NewSystemOverlay creates a new /system overlay mount point.
func NewSystemOverlay(sources []string, target string, opts ...NewPointOption) *Point {
return NewOverlayWithBasePath(sources, target, constants.SystemOverlaysPath, opts...)
}
// NewOverlayWithBasePath creates a new overlay mount point with a base path.
func NewOverlayWithBasePath(sources []string, target, basePath string, opts ...NewPointOption) *Point {
_, overlayPrefix, _ := strings.Cut(target, "/")
overlayPrefix = strings.ReplaceAll(overlayPrefix, "/", "-")
diff := fmt.Sprintf(filepath.Join(basePath, "%s-diff"), overlayPrefix)
workdir := fmt.Sprintf(filepath.Join(basePath, "%s-workdir"), overlayPrefix)
opts = append(opts,
WithData("lowerdir="+strings.Join(sources, ":")),
WithData("upperdir="+diff),
WithData("workdir="+workdir),
WithExtraDirs(diff, workdir),
)
return NewPoint("overlay", target, "overlay", opts...)
}
// PrinterOptions are printer options.
type PrinterOptions struct {
Printer func(string, ...any)
}
// Printf prints a formatted string (or skips if printer is nil).
func (o PrinterOptions) Printf(format string, args ...any) {
if o.Printer != nil {
o.Printer(format, args...)
}
}
// OperationOptions are mount options.
type OperationOptions struct {
PrinterOptions
SkipIfMounted bool
TargetMode os.FileMode
}
// OperationOption is a mount option.
type OperationOption func(*OperationOptions)
// WithSkipIfMounted sets the skip if mounted flag.
func WithSkipIfMounted() OperationOption {
return func(o *OperationOptions) {
o.SkipIfMounted = true
}
}
// WithMountPrinter sets the printer.
func WithMountPrinter(printer func(string, ...any)) OperationOption {
return func(o *OperationOptions) {
o.Printer = printer
}
}
// UnmountOptions is unmount options.
type UnmountOptions struct {
PrinterOptions
}
// UnmountOption is an unmount option.
type UnmountOption func(*UnmountOptions)
// WithUnmountPrinter sets the printer.
func WithUnmountPrinter(printer func(string, ...any)) UnmountOption {
return func(o *UnmountOptions) {
o.Printer = printer
}
}
// IsMounted checks if the mount point is mounted by checking the mount on the target.
func (p *Point) IsMounted() (bool, error) {
f, err := os.Open("/proc/mounts")
if err != nil {
return false, err
}
defer f.Close() //nolint:errcheck
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 2 {
continue
}
mountpoint := fields[1]
if mountpoint == p.target {
return true, nil
}
}
return false, scanner.Err()
}
// Mount the mount point.
//
// Mount returns an unmounter function to unmount the mount point.
func (p *Point) Mount(opts ...OperationOption) (unmounter func() error, err error) {
options := OperationOptions{
TargetMode: 0o755,
}
for _, opt := range opts {
opt(&options)
}
if options.SkipIfMounted {
isMounted, err := p.IsMounted()
if err != nil {
return nil, err
}
// already mounted, return a no-op unmounter
if isMounted {
return func() error {
return nil
}, nil
}
}
for _, dir := range slices.Concat(p.extraDirs, []string{p.target}) {
if err = os.MkdirAll(dir, options.TargetMode); err != nil {
return nil, fmt.Errorf("error creating mount point directory %s: %w", dir, err)
}
}
err = p.retry(p.mount, false, options.PrinterOptions)
if err != nil {
return nil, fmt.Errorf("error mounting %s: %w", p.source, err)
}
if p.shared {
if err = p.share(); err != nil {
return nil, fmt.Errorf("error sharing %s: %w", p.target, err)
}
}
return func() error {
return p.Unmount(WithUnmountPrinter(options.Printer))
}, nil
}
// Unmount the mount point.
func (p *Point) Unmount(opts ...UnmountOption) error {
var options UnmountOptions
for _, opt := range opts {
opt(&options)
}
mounted, err := p.IsMounted()
if err != nil {
return err
}
if !mounted {
return nil
}
return p.retry(func() error {
return p.unmount(options.Printer)
}, true, options.PrinterOptions)
}
// Move the mount point to a new target.
func (p *Point) Move(newTarget string) error {
return unix.Mount(p.target, newTarget, "", unix.MS_MOVE, "")
}
func (p *Point) mount() error {
if err := unix.Mount(p.source, p.target, p.fstype, p.flags, p.data); err != nil {
return err
}
return selinux.SetLabel(p.target, p.selinuxLabel)
}
func (p *Point) unmount(printer func(string, ...any)) error {
return SafeUnmount(context.Background(), printer, p.target)
}
func (p *Point) share() error {
return unix.Mount("", p.target, "", unix.MS_SHARED|unix.MS_REC, "")
}
//nolint:gocyclo
func (p *Point) retry(f func() error, isUnmount bool, printerOptions PrinterOptions) error {
return retry.Constant(5*time.Second, retry.WithUnits(50*time.Millisecond)).Retry(func() error {
if err := f(); err != nil {
switch err {
case unix.EBUSY:
return retry.ExpectedError(err)
case unix.ENOENT, unix.ENXIO:
// if udevd triggers BLKRRPART ioctl, partition device entry might disappear temporarily
return retry.ExpectedError(err)
case unix.EUCLEAN, unix.EIO:
if !isUnmount {
if errRepair := p.repair(printerOptions); errRepair != nil {
return fmt.Errorf("error repairing: %w", errRepair)
}
}
return retry.ExpectedError(err)
case unix.EINVAL:
isMounted, checkErr := p.IsMounted()
if checkErr != nil {
return retry.ExpectedError(checkErr)
}
if !isMounted && !isUnmount {
if errRepair := p.repair(printerOptions); errRepair != nil {
return fmt.Errorf("error repairing: %w", errRepair)
}
return retry.ExpectedError(err)
}
if !isMounted && isUnmount { // if partition is already unmounted, ignore EINVAL
return nil
}
return err
default:
return err
}
}
return nil
})
}